@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
@@ -2,12 +2,15 @@
2
2
 
3
3
  import { applyBudget } from './budget.js';
4
4
  import {
5
+ ARTIFACT_CHUNK_BYTES,
5
6
  BUDGET_PRESETS,
6
7
  DEFAULT_A11Y_MAX_DEPTH,
7
8
  DEFAULT_A11Y_MAX_NODES,
8
9
  DEFAULT_CONSOLE_LIMIT,
9
10
  DEFAULT_DEVICE_SCALE_FACTOR,
10
11
  DEFAULT_EVAL_TIMEOUT_MS,
12
+ DEFAULT_EXTRACT_SETTLE_TIMEOUT_MS,
13
+ DEFAULT_HAR_LIMIT,
11
14
  DEFAULT_LOG_TAIL_LIMIT,
12
15
  DEFAULT_MAX_DEPTH,
13
16
  DEFAULT_MAX_HTML_LENGTH,
@@ -20,11 +23,18 @@ import {
20
23
  DEFAULT_VIEWPORT_HEIGHT,
21
24
  DEFAULT_VIEWPORT_WIDTH,
22
25
  DEFAULT_WAIT_TIMEOUT_MS,
26
+ MAX_EXTRACT_SETTLE_TIMEOUT_MS,
27
+ MAX_ARTIFACT_BYTES,
28
+ MAX_HAR_ENTRIES,
29
+ MAX_SENSITIVE_VALUE_BYTES,
23
30
  } from './defaults.js';
24
31
  import { BridgeError, ERROR_CODES, getErrorRecovery } from './errors.js';
25
32
  import { BRIDGE_METHODS, METHOD_SET, createBridgeMethodGroups } from './registry.js';
26
33
 
27
34
  /** @typedef {import('./types.js').AccessibilityTreeParams} AccessibilityTreeParams */
35
+ /** @typedef {import('./types.js').ArtifactDeleteParams} ArtifactDeleteParams */
36
+ /** @typedef {import('./types.js').ArtifactReadParams} ArtifactReadParams */
37
+ /** @typedef {import('./types.js').AccessRequestParams} AccessRequestParams */
28
38
  /** @typedef {import('./types.js').BridgeFailureResponse} BridgeFailureResponse */
29
39
  /** @typedef {import('./types.js').BridgeMeta} BridgeMeta */
30
40
  /** @typedef {import('./types.js').BridgeMethod} BridgeMethod */
@@ -32,32 +42,48 @@ import { BRIDGE_METHODS, METHOD_SET, createBridgeMethodGroups } from './registry
32
42
  /** @typedef {import('./types.js').BridgeSuccessResponse} BridgeSuccessResponse */
33
43
  /** @typedef {import('./types.js').CheckedActionParams} CheckedActionParams */
34
44
  /** @typedef {import('./types.js').CdpDispatchKeyEventParams} CdpDispatchKeyEventParams */
45
+ /** @typedef {import('./types.js').CdpDomSnapshotParams} CdpDomSnapshotParams */
35
46
  /** @typedef {import('./types.js').CdpNodeIdParams} CdpNodeIdParams */
36
47
  /** @typedef {import('./types.js').ConsoleParams} ConsoleParams */
37
48
  /** @typedef {import('./types.js').DomQueryParams} DomQueryParams */
49
+ /** @typedef {import('./types.js').DomBaselineCreateParams} DomBaselineCreateParams */
50
+ /** @typedef {import('./types.js').DomBaselineCompareParams} DomBaselineCompareParams */
51
+ /** @typedef {import('./types.js').DomBaselineHandleParams} DomBaselineHandleParams */
38
52
  /** @typedef {import('./types.js').DragParams} DragParams */
39
53
  /** @typedef {import('./types.js').EvaluateParams} EvaluateParams */
54
+ /** @typedef {import('./types.js').ExtractContentParams} ExtractContentParams */
40
55
  /** @typedef {import('./types.js').FindByRoleParams} FindByRoleParams */
41
56
  /** @typedef {import('./types.js').FindByTextParams} FindByTextParams */
42
57
  /** @typedef {import('./types.js').GetHtmlParams} GetHtmlParams */
43
58
  /** @typedef {import('./types.js').HoverParams} HoverParams */
59
+ /** @typedef {import('./types.js').HarExportParams} HarExportParams */
60
+ /** @typedef {import('./types.js').HandleDialogParams} HandleDialogParams */
44
61
  /** @typedef {import('./types.js').InputActionParams} InputActionParams */
45
62
  /** @typedef {import('./types.js').LogTailParams} LogTailParams */
46
63
  /** @typedef {import('./types.js').NavigationActionParams} NavigationActionParams */
47
64
  /** @typedef {import('./types.js').NetworkInterceptAddParams} NetworkInterceptAddParams */
48
65
  /** @typedef {import('./types.js').NetworkParams} NetworkParams */
49
66
  /** @typedef {import('./types.js').NormalizedAccessibilityTreeParams} NormalizedAccessibilityTreeParams */
67
+ /** @typedef {import('./types.js').NormalizedArtifactReadParams} NormalizedArtifactReadParams */
68
+ /** @typedef {import('./types.js').NormalizedAccessRequestParams} NormalizedAccessRequestParams */
50
69
  /** @typedef {import('./types.js').NormalizedCheckedAction} NormalizedCheckedAction */
51
70
  /** @typedef {import('./types.js').NormalizedCdpDispatchKeyEventParams} NormalizedCdpDispatchKeyEventParams */
71
+ /** @typedef {import('./types.js').NormalizedCdpDomSnapshotParams} NormalizedCdpDomSnapshotParams */
52
72
  /** @typedef {import('./types.js').NormalizedCdpNodeIdParams} NormalizedCdpNodeIdParams */
53
73
  /** @typedef {import('./types.js').NormalizedConsoleParams} NormalizedConsoleParams */
54
74
  /** @typedef {import('./types.js').NormalizedDomQuery} NormalizedDomQuery */
75
+ /** @typedef {import('./types.js').NormalizedDomBaselineCreateParams} NormalizedDomBaselineCreateParams */
76
+ /** @typedef {import('./types.js').NormalizedDomBaselineCompareParams} NormalizedDomBaselineCompareParams */
77
+ /** @typedef {import('./types.js').NormalizedDomBaselineHandleParams} NormalizedDomBaselineHandleParams */
55
78
  /** @typedef {import('./types.js').NormalizedDragParams} NormalizedDragParams */
56
79
  /** @typedef {import('./types.js').NormalizedEvaluateParams} NormalizedEvaluateParams */
80
+ /** @typedef {import('./types.js').NormalizedExtractContentParams} NormalizedExtractContentParams */
57
81
  /** @typedef {import('./types.js').NormalizedFindByRoleParams} NormalizedFindByRoleParams */
58
82
  /** @typedef {import('./types.js').NormalizedFindByTextParams} NormalizedFindByTextParams */
59
83
  /** @typedef {import('./types.js').NormalizedGetHtmlParams} NormalizedGetHtmlParams */
60
84
  /** @typedef {import('./types.js').NormalizedHoverParams} NormalizedHoverParams */
85
+ /** @typedef {import('./types.js').NormalizedHarExportParams} NormalizedHarExportParams */
86
+ /** @typedef {import('./types.js').NormalizedHandleDialogParams} NormalizedHandleDialogParams */
61
87
  /** @typedef {import('./types.js').NormalizedInputAction} NormalizedInputAction */
62
88
  /** @typedef {import('./types.js').NormalizedLogTailParams} NormalizedLogTailParams */
63
89
  /** @typedef {import('./types.js').NormalizedNavigationAction} NormalizedNavigationAction */
@@ -66,7 +92,9 @@ import { BRIDGE_METHODS, METHOD_SET, createBridgeMethodGroups } from './registry
66
92
  /** @typedef {import('./types.js').NormalizedPageTextParams} NormalizedPageTextParams */
67
93
  /** @typedef {import('./types.js').NormalizedPatchOperation} NormalizedPatchOperation */
68
94
  /** @typedef {import('./types.js').NormalizedSelectAction} NormalizedSelectAction */
95
+ /** @typedef {import('./types.js').NormalizedScreenshotParams} NormalizedScreenshotParams */
69
96
  /** @typedef {import('./types.js').NormalizedStorageParams} NormalizedStorageParams */
97
+ /** @typedef {import('./types.js').NormalizedSensitiveReadParams} NormalizedSensitiveReadParams */
70
98
  /** @typedef {import('./types.js').NormalizedStyleQuery} NormalizedStyleQuery */
71
99
  /** @typedef {import('./types.js').NormalizedTabCloseParams} NormalizedTabCloseParams */
72
100
  /** @typedef {import('./types.js').NormalizedTabCreateParams} NormalizedTabCreateParams */
@@ -77,7 +105,9 @@ import { BRIDGE_METHODS, METHOD_SET, createBridgeMethodGroups } from './registry
77
105
  /** @typedef {import('./types.js').PageTextParams} PageTextParams */
78
106
  /** @typedef {import('./types.js').PatchOperationParams} PatchOperationParams */
79
107
  /** @typedef {import('./types.js').SelectActionParams} SelectActionParams */
108
+ /** @typedef {import('./types.js').ScreenshotParams} ScreenshotParams */
80
109
  /** @typedef {import('./types.js').StorageParams} StorageParams */
110
+ /** @typedef {import('./types.js').SensitiveReadParams} SensitiveReadParams */
81
111
  /** @typedef {import('./types.js').StyleQueryParams} StyleQueryParams */
82
112
  /** @typedef {import('./types.js').TabCloseParams} TabCloseParams */
83
113
  /** @typedef {import('./types.js').TabCreateParams} TabCreateParams */
@@ -234,6 +264,14 @@ export function createSuccess(id, result, meta = {}) {
234
264
  */
235
265
  export function createFailure(id, code, message, details = null, meta = {}) {
236
266
  const recovery = getErrorRecovery(code);
267
+ const effectiveRecovery =
268
+ recovery && meta.method === 'sensitive.read'
269
+ ? {
270
+ retry: false,
271
+ ...(recovery.alternativeMethod ? { alternativeMethod: recovery.alternativeMethod } : {}),
272
+ hint: `Sensitive reads are never retried automatically. ${recovery.hint}`,
273
+ }
274
+ : recovery;
237
275
  return {
238
276
  id,
239
277
  ok: false,
@@ -242,7 +280,7 @@ export function createFailure(id, code, message, details = null, meta = {}) {
242
280
  code,
243
281
  message,
244
282
  details,
245
- ...(recovery && { recovery }),
283
+ ...(effectiveRecovery && { recovery: effectiveRecovery }),
246
284
  },
247
285
  meta: {
248
286
  protocol_version: getProtocolVersion(),
@@ -334,10 +372,28 @@ export function validateBridgeRequest(request) {
334
372
  typeof meta.protocol_version === 'string' ? meta.protocol_version : getProtocolVersion(),
335
373
  token_budget: typeof meta.token_budget === 'number' ? meta.token_budget : null,
336
374
  source: meta.source === 'cli' || meta.source === 'mcp' ? meta.source : undefined,
375
+ automatic_retry: normalizeAutomaticRetryMeta(meta.source, meta.automatic_retry),
337
376
  },
338
377
  };
339
378
  }
340
379
 
380
+ /**
381
+ * @param {unknown} source
382
+ * @param {unknown} value
383
+ * @returns {{ attempt: 2, reason: 'retryable_error' } | undefined}
384
+ */
385
+ function normalizeAutomaticRetryMeta(source, value) {
386
+ if (source !== 'mcp' || !isPlainRecord(value)) return undefined;
387
+ const keys = Object.keys(value).sort();
388
+ return keys.length === 2 &&
389
+ keys[0] === 'attempt' &&
390
+ keys[1] === 'reason' &&
391
+ value.attempt === 2 &&
392
+ value.reason === 'retryable_error'
393
+ ? { attempt: 2, reason: 'retryable_error' }
394
+ : undefined;
395
+ }
396
+
341
397
  /**
342
398
  * Normalize request params early so malformed payloads fail at the shared
343
399
  * protocol boundary before daemon or extension dispatch.
@@ -348,22 +404,39 @@ export function validateBridgeRequest(request) {
348
404
  */
349
405
  function normalizeRequestParams(method, params) {
350
406
  switch (method) {
407
+ case 'access.request':
408
+ return normalizeAccessRequestParams(params);
351
409
  case 'log.tail':
352
410
  return normalizeLogTailParams(params);
353
411
  case 'dom.query':
354
412
  return normalizeDomQuery(params);
413
+ case 'dom.baseline.create':
414
+ return normalizeDomBaselineCreateParams(params);
415
+ case 'dom.baseline.compare':
416
+ return normalizeDomBaselineCompareParams(params);
417
+ case 'dom.baseline.describe':
418
+ case 'dom.baseline.release':
419
+ return normalizeDomBaselineHandleParams(params);
355
420
  case 'page.evaluate':
356
421
  return normalizeEvaluateParams(params);
357
422
  case 'page.get_console':
358
423
  return normalizeConsoleParams(params);
424
+ case 'page.handle_dialog':
425
+ return normalizeHandleDialogParams(params);
359
426
  case 'page.wait_for_load_state':
360
427
  return normalizeWaitForLoadStateParams(params);
361
428
  case 'page.get_storage':
362
429
  return normalizeStorageParams(params);
430
+ case 'sensitive.read':
431
+ return normalizeSensitiveReadParams(params);
363
432
  case 'page.get_text':
364
433
  return normalizePageTextParams(params);
434
+ case 'page.extract_content':
435
+ return normalizeExtractContentParams(params);
365
436
  case 'page.get_network':
366
437
  return normalizeNetworkParams(params);
438
+ case 'network.export_har':
439
+ return normalizeHarExportParams(params);
367
440
  case 'network.intercept.add':
368
441
  return normalizeNetworkInterceptAddParams(params);
369
442
  case 'navigation.navigate':
@@ -399,6 +472,8 @@ function normalizeRequestParams(method, params) {
399
472
  case 'cdp.get_box_model':
400
473
  case 'cdp.get_computed_styles_for_node':
401
474
  return normalizeCdpNodeIdParams(params);
475
+ case 'cdp.get_dom_snapshot':
476
+ return normalizeCdpDomSnapshotParams(params);
402
477
  case 'input.set_checked':
403
478
  return normalizeCheckedAction(params);
404
479
  case 'input.select_option':
@@ -407,6 +482,19 @@ function normalizeRequestParams(method, params) {
407
482
  return normalizeHoverParams(params);
408
483
  case 'input.drag':
409
484
  return normalizeDragParams(params);
485
+ case 'screenshot.capture_region':
486
+ return { ...params, ...normalizeScreenshotParams(params) };
487
+ case 'screenshot.capture_element':
488
+ case 'screenshot.capture_full_page':
489
+ return {
490
+ ...params,
491
+ ...normalizeScreenshotParams(params),
492
+ ...(params.scale === undefined ? { scale: undefined } : {}),
493
+ };
494
+ case 'artifact.read':
495
+ return normalizeArtifactReadParams(params);
496
+ case 'artifact.delete':
497
+ return normalizeArtifactDeleteParams(params);
410
498
  case 'patch.apply_styles':
411
499
  case 'patch.apply_dom':
412
500
  case 'patch.list':
@@ -422,6 +510,28 @@ function normalizeRequestParams(method, params) {
422
510
  }
423
511
  }
424
512
 
513
+ /**
514
+ * @param {AccessRequestParams} [params={}]
515
+ * @returns {NormalizedAccessRequestParams}
516
+ */
517
+ export function normalizeAccessRequestParams(params = {}) {
518
+ const intent = params.intent ?? 'general';
519
+ if (
520
+ intent !== 'inspect' &&
521
+ intent !== 'interact' &&
522
+ intent !== 'capture' &&
523
+ intent !== 'navigate' &&
524
+ intent !== 'debugger' &&
525
+ intent !== 'general'
526
+ ) {
527
+ throw new BridgeError(
528
+ ERROR_CODES.INVALID_REQUEST,
529
+ 'intent must be one of inspect, interact, capture, navigate, debugger, or general.'
530
+ );
531
+ }
532
+ return { intent };
533
+ }
534
+
425
535
  /**
426
536
  * Return the browser-operation timeout for methods that wait asynchronously.
427
537
  * This lets clients and the daemon keep transport deadlines outside the
@@ -442,6 +552,10 @@ export function getBridgeOperationTimeoutMs(method, params = {}) {
442
552
  return normalizeWaitForLoadStateParams(params).timeoutMs;
443
553
  case 'page.evaluate':
444
554
  return normalizeEvaluateParams(params).timeoutMs;
555
+ case 'page.extract_content': {
556
+ const normalized = normalizeExtractContentParams(params);
557
+ return normalized.consistency === 'settled' ? normalized.settleTimeoutMs : null;
558
+ }
445
559
  case 'dom.wait_for':
446
560
  return clampInt(params.timeoutMs, 100, 30_000, 5_000);
447
561
  default:
@@ -464,6 +578,82 @@ export function normalizeDomQuery(params = {}) {
464
578
  };
465
579
  }
466
580
 
581
+ /**
582
+ * @param {DomBaselineCreateParams} [params={}]
583
+ * @returns {NormalizedDomBaselineCreateParams}
584
+ */
585
+ export function normalizeDomBaselineCreateParams(params = {}) {
586
+ const selector = typeof params.selector === 'string' ? params.selector.trim() : '';
587
+ if (selector.length > 2_048) {
588
+ throw new BridgeError(
589
+ ERROR_CODES.INVALID_REQUEST,
590
+ 'selector must not exceed 2,048 characters.'
591
+ );
592
+ }
593
+
594
+ const attributeAllowlist = Array.isArray(params.attributeAllowlist)
595
+ ? [
596
+ ...new Set(
597
+ params.attributeAllowlist
598
+ .filter((name) => typeof name === 'string')
599
+ .map((name) => name.trim().toLowerCase())
600
+ .filter(
601
+ (name) =>
602
+ name.length > 0 &&
603
+ name.length <= 128 &&
604
+ name !== 'value' &&
605
+ !name.startsWith('on') &&
606
+ /^[a-z_][a-z0-9_.:-]*$/u.test(name)
607
+ )
608
+ ),
609
+ ]
610
+ : [];
611
+ if (attributeAllowlist.length > 16) {
612
+ throw new BridgeError(
613
+ ERROR_CODES.INVALID_REQUEST,
614
+ 'attributeAllowlist must contain at most 16 unique attribute names.'
615
+ );
616
+ }
617
+
618
+ return {
619
+ selector: selector || 'body',
620
+ maxNodes: clampInt(params.maxNodes, 1, 1_000, 100),
621
+ maxDepth: clampInt(params.maxDepth, 1, 20, 8),
622
+ textBudget: clampInt(params.textBudget, 32, 1_000, 160),
623
+ attributeAllowlist,
624
+ };
625
+ }
626
+
627
+ /**
628
+ * @param {DomBaselineCompareParams} [params={}]
629
+ * @returns {NormalizedDomBaselineCompareParams}
630
+ */
631
+ export function normalizeDomBaselineCompareParams(params = {}) {
632
+ return {
633
+ baselineId: normalizeDomBaselineId(params.baselineId),
634
+ maxChanges: clampInt(params.maxChanges, 1, 200, 50),
635
+ };
636
+ }
637
+
638
+ /**
639
+ * @param {DomBaselineHandleParams} [params={}]
640
+ * @returns {NormalizedDomBaselineHandleParams}
641
+ */
642
+ export function normalizeDomBaselineHandleParams(params = {}) {
643
+ return { baselineId: normalizeDomBaselineId(params.baselineId) };
644
+ }
645
+
646
+ /**
647
+ * @param {unknown} value
648
+ * @returns {string}
649
+ */
650
+ function normalizeDomBaselineId(value) {
651
+ if (typeof value !== 'string' || !/^baseline_[A-Za-z0-9_-]{32,64}$/u.test(value)) {
652
+ throw new BridgeError(ERROR_CODES.INVALID_REQUEST, 'baselineId is invalid.');
653
+ }
654
+ return value;
655
+ }
656
+
467
657
  /**
468
658
  * @param {StyleQueryParams} [params={}]
469
659
  * @returns {NormalizedStyleQuery}
@@ -499,6 +689,17 @@ function normalizeTarget(target) {
499
689
  };
500
690
  }
501
691
 
692
+ /**
693
+ * @param {unknown} value
694
+ * @returns {import('./types.js').InputExecutionMode}
695
+ */
696
+ function normalizeInputExecutionMode(value) {
697
+ if (value !== undefined && value !== 'dom' && value !== 'cdp') {
698
+ throw new BridgeError(ERROR_CODES.INVALID_REQUEST, 'executionMode must be either dom or cdp.');
699
+ }
700
+ return value === 'cdp' ? 'cdp' : 'dom';
701
+ }
702
+
502
703
  /**
503
704
  * @param {InputActionParams} [params={}]
504
705
  * @returns {NormalizedInputAction}
@@ -527,6 +728,8 @@ export function normalizeInputAction(params = {}) {
527
728
  modifiers: Array.isArray(params.modifiers)
528
729
  ? params.modifiers.filter((modifier) => typeof modifier === 'string' && modifier.trim())
529
730
  : [],
731
+ executionMode: normalizeInputExecutionMode(params.executionMode),
732
+ recoverStale: params.recoverStale === true,
530
733
  };
531
734
  }
532
735
 
@@ -592,6 +795,25 @@ export function normalizeCdpNodeIdParams(params = {}) {
592
795
  };
593
796
  }
594
797
 
798
+ /**
799
+ * @param {CdpDomSnapshotParams} [params={}]
800
+ * @returns {NormalizedCdpDomSnapshotParams}
801
+ */
802
+ export function normalizeCdpDomSnapshotParams(params = {}) {
803
+ const computedStyles = Array.isArray(params.computedStyles)
804
+ ? params.computedStyles.filter(
805
+ (property) => typeof property === 'string' && property.length > 0 && property.length <= 128
806
+ )
807
+ : [];
808
+ if (computedStyles.length > 100) {
809
+ throw new BridgeError(
810
+ ERROR_CODES.INVALID_REQUEST,
811
+ 'computedStyles must contain at most 100 CSS property names.'
812
+ );
813
+ }
814
+ return { computedStyles };
815
+ }
816
+
595
817
  /**
596
818
  * @param {CheckedActionParams} [params={}]
597
819
  * @returns {NormalizedCheckedAction}
@@ -604,6 +826,8 @@ export function normalizeCheckedAction(params = {}) {
604
826
  : undefined
605
827
  ),
606
828
  checked: params.checked !== false,
829
+ executionMode: normalizeInputExecutionMode(params.executionMode),
830
+ recoverStale: params.recoverStale === true,
607
831
  };
608
832
  }
609
833
 
@@ -629,6 +853,8 @@ export function normalizeSelectAction(params = {}) {
629
853
  .map((index) => Number(index))
630
854
  .filter((index) => Number.isInteger(index) && index >= 0)
631
855
  : [],
856
+ executionMode: normalizeInputExecutionMode(params.executionMode),
857
+ recoverStale: params.recoverStale === true,
632
858
  };
633
859
  }
634
860
 
@@ -682,13 +908,30 @@ export function normalizeNavigationAction(params = {}) {
682
908
  * @returns {NormalizedPatchOperation}
683
909
  */
684
910
  export function normalizePatchOperation(params = {}) {
911
+ const operation = typeof params.operation === 'string' ? params.operation : null;
912
+ if (
913
+ operation !== null &&
914
+ ![
915
+ 'set_text',
916
+ 'set_attribute',
917
+ 'remove_attribute',
918
+ 'add_class',
919
+ 'remove_class',
920
+ 'toggle_class',
921
+ ].includes(operation)
922
+ ) {
923
+ throw new BridgeError(
924
+ ERROR_CODES.INVALID_REQUEST,
925
+ `Unsupported DOM patch operation: ${operation}`
926
+ );
927
+ }
685
928
  return {
686
929
  patchId: typeof params.patchId === 'string' ? params.patchId : null,
687
930
  target:
688
931
  params.target && typeof params.target === 'object'
689
932
  ? /** @type {Record<string, unknown>} */ (params.target)
690
933
  : {},
691
- operation: typeof params.operation === 'string' ? params.operation : null,
934
+ operation,
692
935
  name: typeof params.name === 'string' ? params.name : null,
693
936
  declarations:
694
937
  params.declarations && typeof params.declarations === 'object'
@@ -739,6 +982,62 @@ export function normalizeConsoleParams(params = {}) {
739
982
  };
740
983
  }
741
984
 
985
+ /**
986
+ * @param {HandleDialogParams} [params={}]
987
+ * @returns {NormalizedHandleDialogParams}
988
+ */
989
+ export function normalizeHandleDialogParams(params = {}) {
990
+ const action = params.action ?? 'inspect';
991
+ if (action !== 'inspect' && action !== 'accept' && action !== 'dismiss') {
992
+ throw new BridgeError(
993
+ ERROR_CODES.INVALID_REQUEST,
994
+ 'action must be one of inspect, accept, or dismiss.'
995
+ );
996
+ }
997
+ if (
998
+ params.promptText !== undefined &&
999
+ params.promptText !== null &&
1000
+ typeof params.promptText !== 'string'
1001
+ ) {
1002
+ throw new BridgeError(ERROR_CODES.INVALID_REQUEST, 'promptText must be a string.');
1003
+ }
1004
+ if (typeof params.promptText === 'string' && params.promptText.length > 10_000) {
1005
+ throw new BridgeError(
1006
+ ERROR_CODES.INVALID_REQUEST,
1007
+ 'promptText must not exceed 10,000 characters.'
1008
+ );
1009
+ }
1010
+ if (params.promptText !== undefined && params.promptText !== null && action !== 'accept') {
1011
+ throw new BridgeError(
1012
+ ERROR_CODES.INVALID_REQUEST,
1013
+ 'promptText is only valid when action is accept.'
1014
+ );
1015
+ }
1016
+ if (
1017
+ params.expectedDialogId !== undefined &&
1018
+ params.expectedDialogId !== null &&
1019
+ (typeof params.expectedDialogId !== 'string' ||
1020
+ params.expectedDialogId.length === 0 ||
1021
+ params.expectedDialogId.length > 128)
1022
+ ) {
1023
+ throw new BridgeError(
1024
+ ERROR_CODES.INVALID_REQUEST,
1025
+ 'expectedDialogId must be a non-empty string of at most 128 characters.'
1026
+ );
1027
+ }
1028
+ if (action === 'inspect' && params.expectedDialogId != null) {
1029
+ throw new BridgeError(
1030
+ ERROR_CODES.INVALID_REQUEST,
1031
+ 'expectedDialogId is only valid when action is accept or dismiss.'
1032
+ );
1033
+ }
1034
+ return {
1035
+ action,
1036
+ promptText: typeof params.promptText === 'string' ? params.promptText : null,
1037
+ expectedDialogId: typeof params.expectedDialogId === 'string' ? params.expectedDialogId : null,
1038
+ };
1039
+ }
1040
+
742
1041
  /**
743
1042
  * @param {WaitForParams} [params={}]
744
1043
  * @returns {NormalizedWaitForParams}
@@ -823,6 +1122,8 @@ export function normalizeHoverParams(params = {}) {
823
1122
  modifiers: Array.isArray(params.modifiers)
824
1123
  ? params.modifiers.filter((modifier) => typeof modifier === 'string' && modifier.trim())
825
1124
  : [],
1125
+ executionMode: normalizeInputExecutionMode(params.executionMode),
1126
+ recoverStale: params.recoverStale === true,
826
1127
  };
827
1128
  }
828
1129
 
@@ -844,6 +1145,8 @@ export function normalizeDragParams(params = {}) {
844
1145
  ),
845
1146
  offsetX: Number.isFinite(Number(params.offsetX)) ? Number(params.offsetX) : 0,
846
1147
  offsetY: Number.isFinite(Number(params.offsetY)) ? Number(params.offsetY) : 0,
1148
+ executionMode: normalizeInputExecutionMode(params.executionMode),
1149
+ recoverStale: params.recoverStale === true,
847
1150
  };
848
1151
  }
849
1152
 
@@ -852,9 +1155,42 @@ export function normalizeDragParams(params = {}) {
852
1155
  * @returns {NormalizedStorageParams}
853
1156
  */
854
1157
  export function normalizeStorageParams(params = {}) {
1158
+ const keys = Array.isArray(params.keys)
1159
+ ? params.keys.filter((key) => typeof key === 'string')
1160
+ : null;
1161
+ if (keys && keys.length > 100) {
1162
+ throw new BridgeError(ERROR_CODES.INVALID_REQUEST, 'keys must contain at most 100 strings.');
1163
+ }
855
1164
  return {
856
1165
  type: params.type === 'session' ? 'session' : 'local',
857
- keys: Array.isArray(params.keys) ? params.keys.filter((k) => typeof k === 'string') : null,
1166
+ keys,
1167
+ };
1168
+ }
1169
+
1170
+ /**
1171
+ * @param {Record<string, unknown>} params
1172
+ * @returns {NormalizedSensitiveReadParams}
1173
+ */
1174
+ export function normalizeSensitiveReadParams(params) {
1175
+ if (params?.source !== 'local_storage' && params?.source !== 'session_storage') {
1176
+ throw new BridgeError(
1177
+ ERROR_CODES.INVALID_REQUEST,
1178
+ 'source must be local_storage or session_storage.'
1179
+ );
1180
+ }
1181
+ if (typeof params.key !== 'string') {
1182
+ throw new BridgeError(ERROR_CODES.INVALID_REQUEST, 'key must be a string.');
1183
+ }
1184
+ if (params.key.length > 2_048) {
1185
+ throw new BridgeError(
1186
+ ERROR_CODES.INVALID_REQUEST,
1187
+ 'key must contain at most 2,048 characters.'
1188
+ );
1189
+ }
1190
+ return {
1191
+ source: params.source,
1192
+ key: params.key,
1193
+ maxBytes: MAX_SENSITIVE_VALUE_BYTES,
858
1194
  };
859
1195
  }
860
1196
 
@@ -863,12 +1199,98 @@ export function normalizeStorageParams(params = {}) {
863
1199
  * @returns {NormalizedWaitForLoadStateParams}
864
1200
  */
865
1201
  export function normalizeWaitForLoadStateParams(params = {}) {
1202
+ const url = typeof params.url === 'string' && params.url.length > 0 ? params.url : null;
1203
+ const requestedMatch = params.urlMatch ?? (url ? 'exact' : null);
1204
+ if (params.url !== undefined && params.url !== null && typeof params.url !== 'string') {
1205
+ throw new BridgeError(ERROR_CODES.INVALID_REQUEST, 'url must be a string.');
1206
+ }
1207
+ if (params.urlMatch !== undefined && params.urlMatch !== null && !url) {
1208
+ throw new BridgeError(ERROR_CODES.INVALID_REQUEST, 'url is required when urlMatch is set.');
1209
+ }
1210
+ if (
1211
+ requestedMatch !== null &&
1212
+ requestedMatch !== 'exact' &&
1213
+ requestedMatch !== 'contains' &&
1214
+ requestedMatch !== 'regex'
1215
+ ) {
1216
+ throw new BridgeError(
1217
+ ERROR_CODES.INVALID_REQUEST,
1218
+ 'urlMatch must be one of exact, contains, or regex.'
1219
+ );
1220
+ }
1221
+ if (url && url.length > 2_048) {
1222
+ throw new BridgeError(
1223
+ ERROR_CODES.INVALID_REQUEST,
1224
+ 'url condition must not exceed 2,048 characters.'
1225
+ );
1226
+ }
1227
+ if (requestedMatch === 'regex' && url) {
1228
+ validateSafeUrlRegex(url);
1229
+ }
866
1230
  return {
867
1231
  waitForLoad: params.waitForLoad !== false,
868
1232
  timeoutMs: clampInt(params.timeoutMs, 500, 120_000, DEFAULT_NAV_TIMEOUT_MS),
1233
+ url,
1234
+ urlMatch: requestedMatch,
869
1235
  };
870
1236
  }
871
1237
 
1238
+ /**
1239
+ * URL waits accept a deliberately linear regular-expression subset. Patterns
1240
+ * may contain literals, escapes, anchors, dots, and character classes, but no
1241
+ * grouping, alternation, or quantifiers. With both pattern and input bounded,
1242
+ * the accepted subset cannot trigger backtracking through repeated matches.
1243
+ *
1244
+ * @param {string} pattern
1245
+ * @returns {void}
1246
+ */
1247
+ function validateSafeUrlRegex(pattern) {
1248
+ if (pattern.length > 256) {
1249
+ throw new BridgeError(
1250
+ ERROR_CODES.INVALID_REQUEST,
1251
+ 'Regex URL condition must not exceed 256 characters.'
1252
+ );
1253
+ }
1254
+ let escaped = false;
1255
+ let inCharacterClass = false;
1256
+ for (let index = 0; index < pattern.length; index += 1) {
1257
+ const character = pattern[index];
1258
+ if (escaped) {
1259
+ if (!inCharacterClass && /[1-9]/.test(character)) {
1260
+ throw new BridgeError(
1261
+ ERROR_CODES.INVALID_REQUEST,
1262
+ 'Regex URL condition uses an unsupported backreference.'
1263
+ );
1264
+ }
1265
+ escaped = false;
1266
+ continue;
1267
+ }
1268
+ if (character === '\\') {
1269
+ escaped = true;
1270
+ continue;
1271
+ }
1272
+ if (character === '[' && !inCharacterClass) {
1273
+ inCharacterClass = true;
1274
+ continue;
1275
+ }
1276
+ if (character === ']' && inCharacterClass) {
1277
+ inCharacterClass = false;
1278
+ continue;
1279
+ }
1280
+ if (!inCharacterClass && '(){}*+?|'.includes(character)) {
1281
+ throw new BridgeError(
1282
+ ERROR_CODES.INVALID_REQUEST,
1283
+ 'Regex URL condition uses unsupported grouping, alternation, or quantifiers.'
1284
+ );
1285
+ }
1286
+ }
1287
+ try {
1288
+ new RegExp(pattern);
1289
+ } catch {
1290
+ throw new BridgeError(ERROR_CODES.INVALID_REQUEST, 'Regex URL condition is invalid.');
1291
+ }
1292
+ }
1293
+
872
1294
  /**
873
1295
  * @param {TabCreateParams} [params={}]
874
1296
  * @returns {NormalizedTabCreateParams}
@@ -914,16 +1336,95 @@ export function normalizeTabCloseParams(params = {}) {
914
1336
  */
915
1337
  export function normalizeAccessibilityTreeParams(params = {}) {
916
1338
  return {
1339
+ selector:
1340
+ typeof params.selector === 'string' && params.selector.trim() ? params.selector.trim() : null,
917
1341
  maxDepth: clampInt(params.maxDepth, 1, 20, DEFAULT_A11Y_MAX_DEPTH),
918
1342
  maxNodes: clampInt(params.maxNodes, 10, 5000, DEFAULT_A11Y_MAX_NODES),
1343
+ compact: params.compact === true,
1344
+ interactiveOnly: params.interactiveOnly === true,
919
1345
  };
920
1346
  }
921
1347
 
1348
+ /**
1349
+ * @param {ScreenshotParams} [params={}]
1350
+ * @returns {NormalizedScreenshotParams}
1351
+ */
1352
+ export function normalizeScreenshotParams(params = {}) {
1353
+ const format = params.format ?? 'png';
1354
+ if (format !== 'png' && format !== 'jpeg' && format !== 'webp') {
1355
+ throw new BridgeError(ERROR_CODES.INVALID_REQUEST, 'format must be png, jpeg, or webp.');
1356
+ }
1357
+ const numericQuality = Number(params.quality);
1358
+ const delivery = params.delivery ?? 'inline';
1359
+ if (delivery !== 'auto' && delivery !== 'inline' && delivery !== 'artifact') {
1360
+ throw new BridgeError(
1361
+ ERROR_CODES.INVALID_REQUEST,
1362
+ 'delivery must be auto, inline, or artifact.'
1363
+ );
1364
+ }
1365
+ return {
1366
+ format,
1367
+ quality:
1368
+ format === 'png' || params.quality === undefined
1369
+ ? null
1370
+ : Number.isFinite(numericQuality)
1371
+ ? Math.min(100, Math.max(0, Math.trunc(numericQuality)))
1372
+ : 80,
1373
+ delivery,
1374
+ scale:
1375
+ Number.isFinite(Number(params.scale)) && Number(params.scale) > 0
1376
+ ? clampNumber(Number(params.scale), 0.1, 4, 1)
1377
+ : 1,
1378
+ };
1379
+ }
1380
+
1381
+ /**
1382
+ * @param {ArtifactReadParams} [params={}]
1383
+ * @returns {NormalizedArtifactReadParams}
1384
+ */
1385
+ export function normalizeArtifactReadParams(params = {}) {
1386
+ return {
1387
+ artifactId: normalizeArtifactId(params.artifactId),
1388
+ offset: clampInt(params.offset, 0, MAX_ARTIFACT_BYTES, 0),
1389
+ maxBytes: clampInt(params.maxBytes, 1, ARTIFACT_CHUNK_BYTES, ARTIFACT_CHUNK_BYTES),
1390
+ };
1391
+ }
1392
+
1393
+ /**
1394
+ * @param {ArtifactDeleteParams} [params={}]
1395
+ * @returns {{ artifactId: string }}
1396
+ */
1397
+ export function normalizeArtifactDeleteParams(params = {}) {
1398
+ return { artifactId: normalizeArtifactId(params.artifactId) };
1399
+ }
1400
+
1401
+ /** @param {unknown} value */
1402
+ function normalizeArtifactId(value) {
1403
+ if (typeof value !== 'string' || !/^art_[A-Za-z0-9_-]{32,64}$/u.test(value)) {
1404
+ throw new BridgeError(ERROR_CODES.INVALID_REQUEST, 'artifactId is invalid.');
1405
+ }
1406
+ return value;
1407
+ }
1408
+
922
1409
  /**
923
1410
  * @param {NetworkParams} [params={}]
924
1411
  * @returns {NormalizedNetworkParams}
925
1412
  */
926
1413
  export function normalizeNetworkParams(params = {}) {
1414
+ const source = params.source ?? 'fetch-xhr';
1415
+ if (source !== 'fetch-xhr' && source !== 'cdp') {
1416
+ throw new BridgeError(ERROR_CODES.INVALID_REQUEST, 'source must be fetch-xhr or cdp.');
1417
+ }
1418
+ const capture = params.capture ?? 'read';
1419
+ if (capture !== 'read' && capture !== 'start' && capture !== 'clear' && capture !== 'stop') {
1420
+ throw new BridgeError(
1421
+ ERROR_CODES.INVALID_REQUEST,
1422
+ 'capture must be read, start, clear, or stop.'
1423
+ );
1424
+ }
1425
+ if (source !== 'cdp' && params.capture !== undefined && params.capture !== 'read') {
1426
+ throw new BridgeError(ERROR_CODES.INVALID_REQUEST, 'capture is only valid with source "cdp".');
1427
+ }
927
1428
  return {
928
1429
  clear: Boolean(params.clear),
929
1430
  limit: clampInt(params.limit, 1, 500, DEFAULT_NETWORK_LIMIT),
@@ -931,6 +1432,56 @@ export function normalizeNetworkParams(params = {}) {
931
1432
  typeof params.urlPattern === 'string' && params.urlPattern.trim()
932
1433
  ? params.urlPattern.trim()
933
1434
  : null,
1435
+ source,
1436
+ capture,
1437
+ };
1438
+ }
1439
+
1440
+ /**
1441
+ * @param {HarExportParams | NormalizedHarExportParams} [params={}]
1442
+ * @returns {NormalizedHarExportParams}
1443
+ */
1444
+ export function normalizeHarExportParams(params = {}) {
1445
+ if (
1446
+ params.limit !== undefined &&
1447
+ (typeof params.limit !== 'number' ||
1448
+ !Number.isSafeInteger(params.limit) ||
1449
+ params.limit < 1 ||
1450
+ params.limit > MAX_HAR_ENTRIES)
1451
+ ) {
1452
+ throw new BridgeError(
1453
+ ERROR_CODES.INVALID_REQUEST,
1454
+ `limit must be an integer between 1 and ${MAX_HAR_ENTRIES}.`
1455
+ );
1456
+ }
1457
+
1458
+ if (
1459
+ params.urlPattern !== undefined &&
1460
+ params.urlPattern !== null &&
1461
+ typeof params.urlPattern !== 'string'
1462
+ ) {
1463
+ throw new BridgeError(ERROR_CODES.INVALID_REQUEST, 'urlPattern must be a string.');
1464
+ }
1465
+ const urlPattern = params.urlPattern?.trim() || null;
1466
+ if (urlPattern && urlPattern.length > 2_048) {
1467
+ throw new BridgeError(
1468
+ ERROR_CODES.INVALID_REQUEST,
1469
+ 'urlPattern must contain at most 2048 characters.'
1470
+ );
1471
+ }
1472
+
1473
+ const delivery = params.delivery ?? 'auto';
1474
+ if (delivery !== 'auto' && delivery !== 'inline' && delivery !== 'artifact') {
1475
+ throw new BridgeError(
1476
+ ERROR_CODES.INVALID_REQUEST,
1477
+ 'delivery must be auto, inline, or artifact.'
1478
+ );
1479
+ }
1480
+
1481
+ return {
1482
+ limit: params.limit ?? DEFAULT_HAR_LIMIT,
1483
+ urlPattern,
1484
+ delivery,
934
1485
  };
935
1486
  }
936
1487
 
@@ -1009,6 +1560,44 @@ export function normalizePageTextParams(params = {}) {
1009
1560
  };
1010
1561
  }
1011
1562
 
1563
+ /**
1564
+ * @param {ExtractContentParams} [params={}]
1565
+ * @returns {NormalizedExtractContentParams}
1566
+ */
1567
+ export function normalizeExtractContentParams(params = {}) {
1568
+ const format = params.format ?? 'text';
1569
+ if (format !== 'text' && format !== 'markdown') {
1570
+ throw new BridgeError(ERROR_CODES.INVALID_REQUEST, 'format must be text or markdown.');
1571
+ }
1572
+ const consistency = params.consistency ?? 'best_effort';
1573
+ if (consistency !== 'best_effort' && consistency !== 'settled') {
1574
+ throw new BridgeError(
1575
+ ERROR_CODES.INVALID_REQUEST,
1576
+ 'consistency must be best_effort or settled.'
1577
+ );
1578
+ }
1579
+ const selector = typeof params.selector === 'string' ? params.selector.trim() : '';
1580
+ if (selector.length > 2048) {
1581
+ throw new BridgeError(
1582
+ ERROR_CODES.INVALID_REQUEST,
1583
+ 'selector must not exceed 2,048 characters.'
1584
+ );
1585
+ }
1586
+ return {
1587
+ format,
1588
+ selector: selector || null,
1589
+ includeMetadata: params.includeMetadata !== false,
1590
+ consistency,
1591
+ textBudget: clampInt(params.textBudget, 100, 100_000, DEFAULT_PAGE_TEXT_BUDGET),
1592
+ settleTimeoutMs: clampInt(
1593
+ params.settleTimeoutMs,
1594
+ 100,
1595
+ MAX_EXTRACT_SETTLE_TIMEOUT_MS,
1596
+ DEFAULT_EXTRACT_SETTLE_TIMEOUT_MS
1597
+ ),
1598
+ };
1599
+ }
1600
+
1012
1601
  /**
1013
1602
  * @param {LogTailParams} [params={}]
1014
1603
  * @returns {NormalizedLogTailParams}
@@ -1071,9 +1660,20 @@ export function createRuntimeContext() {
1071
1660
  'Browser Bridge is off for this window or the page is restricted; if access is off, the first denied call surfaces an Enable cue in the extension UI',
1072
1661
  TAB_MISMATCH: 'Tab closed or not found',
1073
1662
  ELEMENT_STALE: 'Element removed from DOM - re-query',
1663
+ ELEMENT_AMBIGUOUS: 'Input selector matched equally actionable elements - narrow selector',
1664
+ ELEMENT_NOT_ACTIONABLE: 'Input target is hidden, disabled, inert, or not rendered',
1665
+ ELEMENT_OBSCURED: 'Pointer target is blocked by an unrelated element',
1666
+ ELEMENT_NOT_FOUND: 'Input selector did not match an element',
1667
+ DOM_BASELINE_NOT_FOUND: 'DOM baseline is missing, expired, or released',
1668
+ DOM_BASELINE_INVALIDATED: 'DOM baseline belongs to a different document or scope',
1669
+ DOM_BASELINE_QUOTA_EXCEEDED: 'DOM baseline retention quota was exceeded',
1670
+ INPUT_UNSUPPORTED: 'Execution mode does not support this input operation',
1671
+ INPUT_INVALID_TARGET: 'Input target is incompatible with the requested operation',
1672
+ INPUT_FOCUS_CHANGED: 'Focus moved before native text dispatch',
1673
+ DIALOG_NOT_OPEN: 'No JavaScript dialog is currently observable',
1074
1674
  INVALID_REQUEST: 'Malformed method or params',
1075
1675
  TIMEOUT: 'Operation exceeded time limit',
1076
- RATE_LIMITED: 'Too many requests - back off',
1676
+ CONTENT_SCRIPT_UNAVAILABLE: 'Page is restricted or cannot host the content script',
1077
1677
  INTERNAL_ERROR: 'Unexpected extension error',
1078
1678
  EXTENSION_DISCONNECTED: 'Extension not connected to daemon - check Chrome',
1079
1679
  },
@@ -1081,6 +1681,7 @@ export function createRuntimeContext() {
1081
1681
  'dom.query quick budget first; widen only if truncated',
1082
1682
  "Reuse elementRef; don't re-query",
1083
1683
  'Set attributeAllowlist for focused DOM reads',
1684
+ 'Use dom.baseline.create / compare for bounded semantic DOM change detection; release when done',
1084
1685
  'patch.apply_styles before patch.apply_dom',
1085
1686
  'Verify with get_box_model not screenshots',
1086
1687
  'batch independent reads',
@@ -1091,10 +1692,10 @@ export function createRuntimeContext() {
1091
1692
  'If a tab-bound call returns ACCESS_DENIED because access is off, ask the user to click Enable in the Browser Bridge popup or side panel, then retry once',
1092
1693
  'dom.wait_for after HMR / navigation to detect page updates',
1093
1694
  'page.get_console to catch runtime errors after interactions',
1094
- 'page.get_network to inspect XHR/fetch API calls',
1695
+ 'page.get_network for fetch/XHR reads or explicit all-resource CDP capture',
1095
1696
  'page.get_text for full-page content extraction',
1096
1697
  'input.hover before screenshot to inspect hover states',
1097
- 'performance.get_metrics for Core Web Vitals and load timing',
1698
+ 'performance.get_metrics returns raw Chrome/CDP counters whose names and units vary; it is a point sample with no BBX navigation window or LCP/CLS/INP measurement',
1098
1699
  'viewport.resize to test responsive layouts',
1099
1700
  'Prefer screenshot.capture_element, or a tight screenshot.capture_region when element capture cannot express the needed area',
1100
1701
  'page.get_storage reads localStorage/sessionStorage without evaluate',
@@ -1119,12 +1720,17 @@ export function createRuntimeContext() {
1119
1720
  maxHtmlLength: { min: 32, max: 50000, default: DEFAULT_MAX_HTML_LENGTH },
1120
1721
  a11yMaxNodes: { min: 10, max: 5000, default: DEFAULT_A11Y_MAX_NODES },
1121
1722
  networkLimit: { min: 1, max: 500, default: DEFAULT_NETWORK_LIMIT },
1723
+ harLimit: { min: 1, max: MAX_HAR_ENTRIES, default: DEFAULT_HAR_LIMIT },
1122
1724
  consoleLimit: { min: 1, max: 200, default: DEFAULT_CONSOLE_LIMIT },
1123
1725
  pageTextBudget: {
1124
1726
  min: 100,
1125
1727
  max: 100000,
1126
1728
  default: DEFAULT_PAGE_TEXT_BUDGET,
1127
1729
  },
1730
+ baselineMaxNodes: { min: 1, max: 1000, default: 100 },
1731
+ baselineMaxDepth: { min: 1, max: 20, default: 8 },
1732
+ baselineTextBudget: { min: 32, max: 1000, default: 160 },
1733
+ baselineMaxChanges: { min: 1, max: 200, default: 50 },
1128
1734
  },
1129
1735
  };
1130
1736
  }