@browserbridge/bbx 1.2.0 → 1.4.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 (35) hide show
  1. package/README.md +8 -5
  2. package/package.json +2 -2
  3. package/packages/agent-client/src/cli.js +56 -31
  4. package/packages/agent-client/src/client.js +81 -65
  5. package/packages/agent-client/src/command-registry.js +4 -15
  6. package/packages/agent-client/src/detect.js +3 -3
  7. package/packages/agent-client/src/install.js +3 -7
  8. package/packages/agent-client/src/mcp-config.js +20 -5
  9. package/packages/agent-client/src/runtime.js +7 -41
  10. package/packages/agent-client/src/setup-status.js +3 -13
  11. package/packages/agent-client/src/types.ts +139 -0
  12. package/packages/mcp-server/src/guidance.js +241 -0
  13. package/packages/mcp-server/src/handlers-capture.js +91 -16
  14. package/packages/mcp-server/src/handlers-dom.js +59 -4
  15. package/packages/mcp-server/src/handlers-navigation.js +22 -2
  16. package/packages/mcp-server/src/handlers-page.js +6 -11
  17. package/packages/mcp-server/src/handlers-utils.js +69 -1
  18. package/packages/mcp-server/src/server.js +111 -28
  19. package/packages/native-host/bin/postinstall.js +42 -21
  20. package/packages/native-host/src/auth-token.js +92 -0
  21. package/packages/native-host/src/daemon-process.js +1 -2
  22. package/packages/native-host/src/daemon.js +199 -30
  23. package/packages/native-host/src/framing.js +13 -0
  24. package/packages/native-host/src/native-host.js +25 -7
  25. package/packages/protocol/src/defaults.js +3 -0
  26. package/packages/protocol/src/json-lines.js +29 -1
  27. package/packages/protocol/src/protocol.js +43 -0
  28. package/packages/protocol/src/registry.js +3 -9
  29. package/packages/protocol/src/types.ts +574 -0
  30. package/skills/browser-bridge/SKILL.md +21 -5
  31. package/skills/browser-bridge/agents/openai.yaml +1 -1
  32. package/skills/browser-bridge/references/interaction.md +6 -6
  33. package/skills/browser-bridge/references/protocol.md +57 -54
  34. package/skills/browser-bridge/references/ui-workflows.md +1 -1
  35. package/packages/protocol/src/types.js +0 -626
@@ -2,10 +2,11 @@
2
2
 
3
3
  import net from 'node:net';
4
4
 
5
- import { createFailure, ERROR_CODES } from '../../protocol/src/index.js';
5
+ import { createFailure, ERROR_CODES, MAX_JSON_LINE_BYTES } from '../../protocol/src/index.js';
6
+ import { readBridgeAuthToken } from './auth-token.js';
6
7
  import { createSocketBridgeTransport, getBridgeTransport } from './config.js';
7
8
  import { spawnBridgeDaemonProcess } from './daemon-process.js';
8
- import { createNativeMessageReader, writeJsonLine, writeNativeMessage } from './framing.js';
9
+ import { createNativeMessageReader, createNativeMessageWriter, writeJsonLine } from './framing.js';
9
10
 
10
11
  /** @typedef {import('./config.js').BridgeTransport} BridgeTransport */
11
12
 
@@ -117,11 +118,13 @@ export async function runNativeHost({
117
118
  socketPath = undefined,
118
119
  } = {}) {
119
120
  const resolvedTransport = socketPath ? createSocketBridgeTransport(socketPath) : transport;
121
+ const writeNativeMessageQueued = createNativeMessageWriter(process.stdout);
122
+
120
123
  let socket;
121
124
  try {
122
125
  socket = await connectWithBootstrap(resolvedTransport);
123
126
  } catch (error) {
124
- await writeNativeMessage(process.stdout, {
127
+ await writeNativeMessageQueued({
125
128
  type: 'agent.response',
126
129
  response: createFailure(
127
130
  'native_bootstrap',
@@ -144,11 +147,21 @@ export async function runNativeHost({
144
147
  socket.once('close', cleanupStdinEndListener);
145
148
  socket.once('end', cleanupStdinEndListener);
146
149
  socket.once('error', cleanupStdinEndListener);
147
- await writeJsonLine(socket, { type: 'register', role: 'extension' });
150
+ const authToken = resolvedTransport.type === 'tcp' ? await readBridgeAuthToken() : null;
151
+ await writeJsonLine(socket, {
152
+ type: 'register',
153
+ role: 'extension',
154
+ ...(authToken ? { authToken } : {}),
155
+ });
148
156
 
149
157
  let lineBuffer = '';
150
158
  socket.on('data', (chunk) => {
151
159
  lineBuffer += chunk;
160
+ if (!lineBuffer.includes('\n') && Buffer.byteLength(lineBuffer, 'utf8') > MAX_JSON_LINE_BYTES) {
161
+ console.error(`native-host: daemon JSON line exceeded ${MAX_JSON_LINE_BYTES} bytes`);
162
+ socket.destroy();
163
+ return;
164
+ }
152
165
  while (lineBuffer.includes('\n')) {
153
166
  const index = lineBuffer.indexOf('\n');
154
167
  const line = lineBuffer.slice(0, index).trim();
@@ -156,6 +169,11 @@ export async function runNativeHost({
156
169
  if (!line) {
157
170
  continue;
158
171
  }
172
+ if (Buffer.byteLength(line, 'utf8') > MAX_JSON_LINE_BYTES) {
173
+ console.error(`native-host: daemon JSON line exceeded ${MAX_JSON_LINE_BYTES} bytes`);
174
+ socket.destroy();
175
+ return;
176
+ }
159
177
  let message;
160
178
  try {
161
179
  message = JSON.parse(line);
@@ -164,11 +182,11 @@ export async function runNativeHost({
164
182
  }
165
183
  void (async () => {
166
184
  if (message.type === 'extension.request') {
167
- await writeNativeMessage(process.stdout, message.request);
185
+ await writeNativeMessageQueued(message.request);
168
186
  return;
169
187
  }
170
188
  if (message.type === 'agent.response') {
171
- await writeNativeMessage(process.stdout, {
189
+ await writeNativeMessageQueued({
172
190
  type: 'host.bridge_response',
173
191
  response: message.response,
174
192
  });
@@ -178,7 +196,7 @@ export async function runNativeHost({
178
196
  message.type === 'extension.setup_status.response' ||
179
197
  message.type === 'extension.setup_status.error'
180
198
  ) {
181
- await writeNativeMessage(process.stdout, {
199
+ await writeNativeMessageQueued({
182
200
  type:
183
201
  message.type === 'extension.setup_status.response'
184
202
  ? 'host.setup_status.response'
@@ -24,6 +24,9 @@ export const DEFAULT_DEVICE_SCALE_FACTOR = 0;
24
24
  /** Maximum size of a Chrome native messaging message in bytes. */
25
25
  export const MAX_NATIVE_MESSAGE_BYTES = 1_048_576;
26
26
 
27
+ /** Maximum size of one newline-delimited daemon socket message in bytes. */
28
+ export const MAX_JSON_LINE_BYTES = MAX_NATIVE_MESSAGE_BYTES;
29
+
27
30
  /** Default timeout for a bridge request awaiting an extension response (ms). */
28
31
  export const DEFAULT_DAEMON_PENDING_TIMEOUT_MS = 30_000;
29
32
 
@@ -1,18 +1,42 @@
1
1
  // @ts-check
2
2
 
3
+ import { MAX_JSON_LINE_BYTES } from './defaults.js';
4
+
3
5
  /**
4
6
  * Install a newline-delimited JSON parser on a socket's `data` event.
5
7
  *
6
8
  * @param {import('node:net').Socket} socket
7
9
  * @param {(message: unknown) => void} onMessage
10
+ * @param {{ maxLineBytes?: number, onProtocolError?: (error: Error) => void }} [options]
8
11
  * @returns {void}
9
12
  */
10
- export function parseJsonLines(socket, onMessage) {
13
+ export function parseJsonLines(socket, onMessage, options = {}) {
11
14
  let buffer = '';
15
+ const maxLineBytes =
16
+ typeof options.maxLineBytes === 'number' && Number.isFinite(options.maxLineBytes)
17
+ ? Math.max(1, Math.floor(options.maxLineBytes))
18
+ : MAX_JSON_LINE_BYTES;
12
19
  socket.setEncoding('utf8');
20
+
21
+ /**
22
+ * @param {Error} error
23
+ * @returns {void}
24
+ */
25
+ function fail(error) {
26
+ options.onProtocolError?.(error);
27
+ const destroy = /** @type {{ destroy?: (() => void) | undefined }} */ (socket).destroy;
28
+ if (typeof destroy === 'function') {
29
+ destroy.call(socket);
30
+ }
31
+ }
32
+
13
33
  /** @param {string} chunk */
14
34
  socket.on('data', (chunk) => {
15
35
  buffer += chunk;
36
+ if (!buffer.includes('\n') && Buffer.byteLength(buffer, 'utf8') > maxLineBytes) {
37
+ fail(new Error(`JSON line exceeds ${maxLineBytes} bytes.`));
38
+ return;
39
+ }
16
40
  while (buffer.includes('\n')) {
17
41
  const index = buffer.indexOf('\n');
18
42
  const line = buffer.slice(0, index).trim();
@@ -20,6 +44,10 @@ export function parseJsonLines(socket, onMessage) {
20
44
  if (!line) {
21
45
  continue;
22
46
  }
47
+ if (Buffer.byteLength(line, 'utf8') > maxLineBytes) {
48
+ fail(new Error(`JSON line exceeds ${maxLineBytes} bytes.`));
49
+ return;
50
+ }
23
51
  try {
24
52
  onMessage(JSON.parse(line));
25
53
  } catch {
@@ -7,6 +7,7 @@ import {
7
7
  DEFAULT_A11Y_MAX_NODES,
8
8
  DEFAULT_CONSOLE_LIMIT,
9
9
  DEFAULT_EVAL_TIMEOUT_MS,
10
+ DEFAULT_LOG_TAIL_LIMIT,
10
11
  DEFAULT_MAX_DEPTH,
11
12
  DEFAULT_MAX_HTML_LENGTH,
12
13
  DEFAULT_MAX_NODES,
@@ -29,6 +30,7 @@ import { BRIDGE_METHODS, METHOD_SET, createBridgeMethodGroups } from './registry
29
30
  /** @typedef {import('./types.js').BridgeSuccessResponse} BridgeSuccessResponse */
30
31
  /** @typedef {import('./types.js').CheckedActionParams} CheckedActionParams */
31
32
  /** @typedef {import('./types.js').CdpDispatchKeyEventParams} CdpDispatchKeyEventParams */
33
+ /** @typedef {import('./types.js').CdpNodeIdParams} CdpNodeIdParams */
32
34
  /** @typedef {import('./types.js').ConsoleParams} ConsoleParams */
33
35
  /** @typedef {import('./types.js').DomQueryParams} DomQueryParams */
34
36
  /** @typedef {import('./types.js').DragParams} DragParams */
@@ -38,11 +40,13 @@ import { BRIDGE_METHODS, METHOD_SET, createBridgeMethodGroups } from './registry
38
40
  /** @typedef {import('./types.js').GetHtmlParams} GetHtmlParams */
39
41
  /** @typedef {import('./types.js').HoverParams} HoverParams */
40
42
  /** @typedef {import('./types.js').InputActionParams} InputActionParams */
43
+ /** @typedef {import('./types.js').LogTailParams} LogTailParams */
41
44
  /** @typedef {import('./types.js').NavigationActionParams} NavigationActionParams */
42
45
  /** @typedef {import('./types.js').NetworkParams} NetworkParams */
43
46
  /** @typedef {import('./types.js').NormalizedAccessibilityTreeParams} NormalizedAccessibilityTreeParams */
44
47
  /** @typedef {import('./types.js').NormalizedCheckedAction} NormalizedCheckedAction */
45
48
  /** @typedef {import('./types.js').NormalizedCdpDispatchKeyEventParams} NormalizedCdpDispatchKeyEventParams */
49
+ /** @typedef {import('./types.js').NormalizedCdpNodeIdParams} NormalizedCdpNodeIdParams */
46
50
  /** @typedef {import('./types.js').NormalizedConsoleParams} NormalizedConsoleParams */
47
51
  /** @typedef {import('./types.js').NormalizedDomQuery} NormalizedDomQuery */
48
52
  /** @typedef {import('./types.js').NormalizedDragParams} NormalizedDragParams */
@@ -52,6 +56,7 @@ import { BRIDGE_METHODS, METHOD_SET, createBridgeMethodGroups } from './registry
52
56
  /** @typedef {import('./types.js').NormalizedGetHtmlParams} NormalizedGetHtmlParams */
53
57
  /** @typedef {import('./types.js').NormalizedHoverParams} NormalizedHoverParams */
54
58
  /** @typedef {import('./types.js').NormalizedInputAction} NormalizedInputAction */
59
+ /** @typedef {import('./types.js').NormalizedLogTailParams} NormalizedLogTailParams */
55
60
  /** @typedef {import('./types.js').NormalizedNavigationAction} NormalizedNavigationAction */
56
61
  /** @typedef {import('./types.js').NormalizedNetworkParams} NormalizedNetworkParams */
57
62
  /** @typedef {import('./types.js').NormalizedPageTextParams} NormalizedPageTextParams */
@@ -209,6 +214,12 @@ export function validateBridgeRequest(request) {
209
214
  'session_id is no longer supported. Use tab_id or window-scoped default routing.'
210
215
  );
211
216
  }
217
+ if (
218
+ candidate.params != null &&
219
+ (typeof candidate.params !== 'object' || Array.isArray(candidate.params))
220
+ ) {
221
+ throw new BridgeError(ERROR_CODES.INVALID_REQUEST, 'Request params must be an object.');
222
+ }
212
223
  const parsedTabId = Number(candidate.tab_id);
213
224
 
214
225
  const method = /** @type {BridgeMethod} */ (candidate.method);
@@ -242,6 +253,8 @@ export function validateBridgeRequest(request) {
242
253
  */
243
254
  function normalizeRequestParams(method, params) {
244
255
  switch (method) {
256
+ case 'log.tail':
257
+ return normalizeLogTailParams(params);
245
258
  case 'dom.query':
246
259
  return normalizeDomQuery(params);
247
260
  case 'page.evaluate':
@@ -285,6 +298,9 @@ function normalizeRequestParams(method, params) {
285
298
  return normalizeInputAction(params);
286
299
  case 'cdp.dispatch_key_event':
287
300
  return normalizeCdpDispatchKeyEventParams(params);
301
+ case 'cdp.get_box_model':
302
+ case 'cdp.get_computed_styles_for_node':
303
+ return normalizeCdpNodeIdParams(params);
288
304
  case 'input.set_checked':
289
305
  return normalizeCheckedAction(params);
290
306
  case 'input.select_option':
@@ -429,6 +445,20 @@ export function normalizeCdpDispatchKeyEventParams(params = {}) {
429
445
  };
430
446
  }
431
447
 
448
+ /**
449
+ * @param {CdpNodeIdParams} [params={}]
450
+ * @returns {NormalizedCdpNodeIdParams}
451
+ */
452
+ export function normalizeCdpNodeIdParams(params = {}) {
453
+ if (typeof params.nodeId !== 'number' || !Number.isFinite(params.nodeId)) {
454
+ throw new BridgeError(ERROR_CODES.INVALID_REQUEST, 'nodeId must be a finite number.');
455
+ }
456
+
457
+ return {
458
+ nodeId: params.nodeId,
459
+ };
460
+ }
461
+
432
462
  /**
433
463
  * @param {CheckedActionParams} [params={}]
434
464
  * @returns {NormalizedCheckedAction}
@@ -642,6 +672,9 @@ export function normalizeHoverParams(params = {}) {
642
672
  : undefined
643
673
  ),
644
674
  duration: clampInt(params.duration, 0, 5_000, 0),
675
+ modifiers: Array.isArray(params.modifiers)
676
+ ? params.modifiers.filter((modifier) => typeof modifier === 'string' && modifier.trim())
677
+ : [],
645
678
  };
646
679
  }
647
680
 
@@ -763,6 +796,16 @@ export function normalizePageTextParams(params = {}) {
763
796
  };
764
797
  }
765
798
 
799
+ /**
800
+ * @param {LogTailParams} [params={}]
801
+ * @returns {NormalizedLogTailParams}
802
+ */
803
+ export function normalizeLogTailParams(params = {}) {
804
+ return {
805
+ limit: clampInt(params.limit, 1, 200, DEFAULT_LOG_TAIL_LIMIT),
806
+ };
807
+ }
808
+
766
809
  /**
767
810
  * @param {ViewportResizeParams} [params={}]
768
811
  * @returns {NormalizedViewportResizeParams}
@@ -123,7 +123,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
123
123
  ['kind', 'target'],
124
124
  'trivial'
125
125
  ),
126
- 'log.tail': createRegistryEntry('log.tail', 'system', false, [], 'trivial'),
126
+ 'log.tail': createRegistryEntry('log.tail', 'system', false, ['limit'], 'trivial'),
127
127
  'health.ping': createRegistryEntry('health.ping', 'system', false, [], 'trivial'),
128
128
  'daemon.metrics': createRegistryEntry('daemon.metrics', 'system', false, [], 'trivial'),
129
129
  // tabs — trivial
@@ -413,18 +413,12 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
413
413
  // cdp — high (raw protocol, large payloads)
414
414
  'cdp.get_document': createRegistryEntry('cdp.get_document', 'cdp', true, [], 'high'),
415
415
  'cdp.get_dom_snapshot': createRegistryEntry('cdp.get_dom_snapshot', 'cdp', true, [], 'high'),
416
- 'cdp.get_box_model': createRegistryEntry(
417
- 'cdp.get_box_model',
418
- 'cdp',
419
- true,
420
- ['elementRef'],
421
- 'high'
422
- ),
416
+ 'cdp.get_box_model': createRegistryEntry('cdp.get_box_model', 'cdp', true, ['nodeId'], 'high'),
423
417
  'cdp.get_computed_styles_for_node': createRegistryEntry(
424
418
  'cdp.get_computed_styles_for_node',
425
419
  'cdp',
426
420
  true,
427
- ['elementRef'],
421
+ ['nodeId'],
428
422
  'high'
429
423
  ),
430
424
  'cdp.dispatch_key_event': createRegistryEntry(