@browserbridge/bbx 1.11.1 → 1.11.2
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.
- package/package.json +1 -1
- package/packages/agent-client/src/client.js +110 -37
- package/packages/agent-client/src/runtime.js +1 -1
- package/packages/mcp-server/src/handlers-capture.js +1 -1
- package/packages/mcp-server/src/handlers-navigation.js +14 -21
- package/packages/mcp-server/src/handlers-page.js +16 -20
- package/packages/mcp-server/src/handlers-utils.js +188 -44
- package/packages/mcp-server/src/server.js +2 -1
- package/packages/native-host/src/auth-token.js +90 -28
- package/packages/native-host/src/daemon-process.js +5 -2
- package/packages/native-host/src/daemon.js +117 -22
- package/packages/native-host/src/framing.js +107 -17
- package/packages/native-host/src/native-host.js +4 -0
package/package.json
CHANGED
|
@@ -104,16 +104,37 @@ function shouldAutoRestartDaemonOnVersionMismatch(env = process.env) {
|
|
|
104
104
|
/**
|
|
105
105
|
* @param {net.Socket} socket
|
|
106
106
|
* @param {string} line
|
|
107
|
+
* @param {AbortSignal} signal
|
|
107
108
|
* @returns {Promise<void>}
|
|
108
109
|
*/
|
|
109
|
-
async function writeSocketLine(socket, line) {
|
|
110
|
+
async function writeSocketLine(socket, line, signal) {
|
|
111
|
+
signal.throwIfAborted();
|
|
110
112
|
if (!socket.write(line)) {
|
|
111
|
-
await Promise
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
113
|
+
await new Promise((resolve, reject) => {
|
|
114
|
+
const cleanup = () => {
|
|
115
|
+
socket.off('drain', onDrain);
|
|
116
|
+
socket.off('error', onError);
|
|
117
|
+
socket.off('close', onClose);
|
|
118
|
+
signal.removeEventListener('abort', onAbort);
|
|
119
|
+
};
|
|
120
|
+
const onDrain = () => {
|
|
121
|
+
cleanup();
|
|
122
|
+
resolve(undefined);
|
|
123
|
+
};
|
|
124
|
+
/** @param {unknown} error */
|
|
125
|
+
const onError = (error) => {
|
|
126
|
+
cleanup();
|
|
127
|
+
reject(error);
|
|
128
|
+
};
|
|
129
|
+
const onClose = () => onError(new Error('Bridge socket closed while writing.'));
|
|
130
|
+
const onAbort = () => onError(signal.reason);
|
|
131
|
+
socket.once('drain', onDrain);
|
|
132
|
+
socket.once('error', onError);
|
|
133
|
+
socket.once('close', onClose);
|
|
134
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
135
|
+
if (signal.aborted) onAbort();
|
|
136
|
+
else if (socket.destroyed) onClose();
|
|
137
|
+
});
|
|
117
138
|
}
|
|
118
139
|
}
|
|
119
140
|
|
|
@@ -176,12 +197,31 @@ export class BridgeClient extends EventEmitter {
|
|
|
176
197
|
this.socket = socket;
|
|
177
198
|
try {
|
|
178
199
|
await new Promise((resolve, reject) => {
|
|
179
|
-
|
|
180
|
-
|
|
200
|
+
const cleanup = () => {
|
|
201
|
+
socket.off('connect', onConnect);
|
|
202
|
+
socket.off('error', onError);
|
|
203
|
+
socket.off('close', onClose);
|
|
204
|
+
};
|
|
205
|
+
const onConnect = () => {
|
|
206
|
+
cleanup();
|
|
207
|
+
resolve(undefined);
|
|
208
|
+
};
|
|
209
|
+
/** @param {Error} error */
|
|
210
|
+
const onError = (error) => {
|
|
211
|
+
cleanup();
|
|
212
|
+
reject(error);
|
|
213
|
+
};
|
|
214
|
+
const onClose = () => onError(new Error('Bridge socket closed while connecting.'));
|
|
215
|
+
socket.once('connect', onConnect);
|
|
216
|
+
socket.once('error', onError);
|
|
217
|
+
socket.once('close', onClose);
|
|
181
218
|
});
|
|
219
|
+
if (this.socket !== socket || socket.destroyed) {
|
|
220
|
+
throw new Error('Bridge socket closed while connecting.');
|
|
221
|
+
}
|
|
182
222
|
} catch (error) {
|
|
183
223
|
socket.destroy();
|
|
184
|
-
this.socket = null;
|
|
224
|
+
if (this.socket === socket) this.socket = null;
|
|
185
225
|
throw error;
|
|
186
226
|
}
|
|
187
227
|
|
|
@@ -198,6 +238,7 @@ export class BridgeClient extends EventEmitter {
|
|
|
198
238
|
});
|
|
199
239
|
|
|
200
240
|
parseJsonLines(socket, (raw) => {
|
|
241
|
+
if (this.socket !== socket) return;
|
|
201
242
|
const message = /** @type {ClientMessage} */ (raw);
|
|
202
243
|
if (message.type === 'registered') {
|
|
203
244
|
const pending = this.waiting.get('registered');
|
|
@@ -231,6 +272,7 @@ export class BridgeClient extends EventEmitter {
|
|
|
231
272
|
});
|
|
232
273
|
|
|
233
274
|
socket.on('close', () => {
|
|
275
|
+
if (this.socket !== socket) return;
|
|
234
276
|
this.connected = false;
|
|
235
277
|
this.socket = null;
|
|
236
278
|
this.rejectAllPending(new Error('Bridge socket closed.'));
|
|
@@ -240,35 +282,50 @@ export class BridgeClient extends EventEmitter {
|
|
|
240
282
|
});
|
|
241
283
|
|
|
242
284
|
socket.on('error', (error) => {
|
|
285
|
+
if (this.socket !== socket) return;
|
|
243
286
|
this.rejectAllPending(error);
|
|
244
287
|
// 'close' fires after 'error'; reconnect is triggered there.
|
|
245
288
|
});
|
|
246
289
|
|
|
247
|
-
const
|
|
248
|
-
this.authToken === undefined
|
|
249
|
-
? this.transport.type === 'tcp'
|
|
250
|
-
? await readBridgeAuthToken()
|
|
251
|
-
: null
|
|
252
|
-
: normalizeBridgeAuthToken(this.authToken);
|
|
290
|
+
const registrationWrite = new AbortController();
|
|
253
291
|
try {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
292
|
+
const writePromise = (async () => {
|
|
293
|
+
const authToken =
|
|
294
|
+
this.authToken === undefined
|
|
295
|
+
? this.transport.type === 'tcp'
|
|
296
|
+
? await readBridgeAuthToken()
|
|
297
|
+
: null
|
|
298
|
+
: normalizeBridgeAuthToken(this.authToken);
|
|
299
|
+
await writeSocketLine(
|
|
300
|
+
socket,
|
|
301
|
+
`${JSON.stringify({
|
|
302
|
+
type: 'register',
|
|
303
|
+
role: 'agent',
|
|
304
|
+
clientId: this.clientId,
|
|
305
|
+
...(authToken ? { authToken } : {}),
|
|
306
|
+
})}\n`,
|
|
307
|
+
registrationWrite.signal
|
|
308
|
+
);
|
|
309
|
+
})();
|
|
310
|
+
await Promise.race([registrationPromise, writePromise.then(() => registrationPromise)]);
|
|
311
|
+
if (this.socket !== socket || socket.destroyed) {
|
|
312
|
+
throw new Error('Bridge socket closed while registering.');
|
|
313
|
+
}
|
|
263
314
|
} catch (error) {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
315
|
+
if (this.socket === socket) {
|
|
316
|
+
const pending = this.waiting.get('registered');
|
|
317
|
+
if (pending) {
|
|
318
|
+
clearTimeout(pending.timeoutId);
|
|
319
|
+
this.waiting.delete('registered');
|
|
320
|
+
}
|
|
321
|
+
this.connected = false;
|
|
322
|
+
this.socket = null;
|
|
268
323
|
}
|
|
324
|
+
socket.destroy();
|
|
269
325
|
throw error;
|
|
326
|
+
} finally {
|
|
327
|
+
registrationWrite.abort();
|
|
270
328
|
}
|
|
271
|
-
await registrationPromise;
|
|
272
329
|
|
|
273
330
|
this.protocolCompatibility = null;
|
|
274
331
|
this.protocolWarning = null;
|
|
@@ -553,18 +610,25 @@ export class BridgeClient extends EventEmitter {
|
|
|
553
610
|
});
|
|
554
611
|
});
|
|
555
612
|
|
|
613
|
+
const requestWrite = new AbortController();
|
|
556
614
|
try {
|
|
557
|
-
|
|
558
|
-
|
|
615
|
+
const writePromise = writeSocketLine(
|
|
616
|
+
this.socket,
|
|
617
|
+
`${JSON.stringify({ type: 'agent.request', request })}\n`,
|
|
618
|
+
requestWrite.signal
|
|
619
|
+
);
|
|
620
|
+
const response = /** @type {BridgeResponse} */ (
|
|
621
|
+
await Promise.race([responsePromise, writePromise.then(() => responsePromise)])
|
|
622
|
+
);
|
|
623
|
+
return this.attachProtocolWarning(response);
|
|
624
|
+
} finally {
|
|
625
|
+
requestWrite.abort();
|
|
559
626
|
const pending = this.waiting.get(request.id);
|
|
560
627
|
if (pending) {
|
|
561
628
|
clearTimeout(pending.timeoutId);
|
|
562
629
|
this.waiting.delete(request.id);
|
|
563
630
|
}
|
|
564
|
-
throw error;
|
|
565
631
|
}
|
|
566
|
-
const response = /** @type {BridgeResponse} */ (await responsePromise);
|
|
567
|
-
return this.attachProtocolWarning(response);
|
|
568
632
|
}
|
|
569
633
|
|
|
570
634
|
/**
|
|
@@ -611,11 +675,19 @@ export class BridgeClient extends EventEmitter {
|
|
|
611
675
|
*/
|
|
612
676
|
async close() {
|
|
613
677
|
this.autoReconnect = false; // prevent reconnect on intentional close
|
|
614
|
-
|
|
678
|
+
const socket = this.socket;
|
|
679
|
+
if (!socket) {
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
if (socket.connecting || !this.connected || socket.destroyed || socket.writableNeedDrain) {
|
|
683
|
+
this.socket = null;
|
|
684
|
+
this.connected = false;
|
|
685
|
+
this.rejectAllPending(new Error('Bridge socket closed.'));
|
|
686
|
+
socket.destroy();
|
|
615
687
|
return;
|
|
616
688
|
}
|
|
617
689
|
await new Promise((resolve) => {
|
|
618
|
-
|
|
690
|
+
socket.end(() => resolve(undefined));
|
|
619
691
|
});
|
|
620
692
|
}
|
|
621
693
|
|
|
@@ -697,6 +769,7 @@ export class BridgeClient extends EventEmitter {
|
|
|
697
769
|
const previousAutoReconnect = this.autoReconnect;
|
|
698
770
|
this.autoReconnect = false;
|
|
699
771
|
this.connected = false;
|
|
772
|
+
this.rejectAllPending(new Error('Bridge socket closed.'));
|
|
700
773
|
this.socket = null;
|
|
701
774
|
|
|
702
775
|
if (!socket.destroyed) {
|
|
@@ -204,8 +204,8 @@ function withRequestMeta(source, mcpEra, tokenBudget, automaticRetry) {
|
|
|
204
204
|
*/
|
|
205
205
|
export async function withBridgeClient(callback, options) {
|
|
206
206
|
const client = new BridgeClient(await applyConfiguredAutoUpdate(options));
|
|
207
|
-
await ensureClientConnected(client);
|
|
208
207
|
try {
|
|
208
|
+
await ensureClientConnected(client);
|
|
209
209
|
return await callback(client);
|
|
210
210
|
} finally {
|
|
211
211
|
await client.close();
|
|
@@ -153,7 +153,7 @@ export function createScreenshotResult(response, method) {
|
|
|
153
153
|
return summarizeToolError(new Error(`${method} returned invalid artifact metadata.`));
|
|
154
154
|
}
|
|
155
155
|
return createToolResult(
|
|
156
|
-
`Captured screenshot artifact (${artifact.byteLength} bytes, expires ${artifact.expiresAt ?? 'soon'}). Read it in chunks with
|
|
156
|
+
`Captured screenshot artifact (${artifact.byteLength} bytes, expires ${artifact.expiresAt ?? 'soon'}). Read it in chunks with browser_call method "artifact.read", paging offset until nextOffset is null, then delete it with browser_call method "artifact.delete" when done.`,
|
|
157
157
|
{
|
|
158
158
|
ok: true,
|
|
159
159
|
method,
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
-
import { createBridgeClientForDestination } from '../../agent-client/src/remotes.js';
|
|
4
3
|
import {
|
|
5
4
|
callBridgeTool,
|
|
6
5
|
createToolResult,
|
|
@@ -8,6 +7,7 @@ import {
|
|
|
8
7
|
getToolTokenBudget,
|
|
9
8
|
requestBridgeWithRetry,
|
|
10
9
|
summarizeToolError,
|
|
10
|
+
withMcpRequestClient,
|
|
11
11
|
} from './handlers-utils.js';
|
|
12
12
|
|
|
13
13
|
/** @typedef {import('../../protocol/src/types.js').BridgeMethod} BridgeMethod */
|
|
@@ -28,23 +28,20 @@ export async function handleTabsTool(args) {
|
|
|
28
28
|
}
|
|
29
29
|
const results = await Promise.all(
|
|
30
30
|
destinations.map(async (destination) => {
|
|
31
|
-
/** @type {import('../../agent-client/src/client.js').BridgeClient | null} */
|
|
32
|
-
let client = null;
|
|
33
31
|
try {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
32
|
+
const response = await withMcpRequestClient(
|
|
33
|
+
(client) =>
|
|
34
|
+
requestBridgeWithRetry(
|
|
35
|
+
client,
|
|
36
|
+
'tabs.list',
|
|
37
|
+
{},
|
|
38
|
+
{
|
|
39
|
+
tabId: null,
|
|
40
|
+
source: 'mcp',
|
|
41
|
+
tokenBudget: null,
|
|
42
|
+
}
|
|
43
|
+
),
|
|
44
|
+
{ destinationId: destination.local ? null : destination.id }
|
|
48
45
|
);
|
|
49
46
|
if (!response.ok) {
|
|
50
47
|
return {
|
|
@@ -67,10 +64,6 @@ export async function handleTabsTool(args) {
|
|
|
67
64
|
error: { message: error instanceof Error ? error.message : String(error) },
|
|
68
65
|
tabs: [],
|
|
69
66
|
};
|
|
70
|
-
} finally {
|
|
71
|
-
if (client) {
|
|
72
|
-
await client.close().catch(() => {});
|
|
73
|
-
}
|
|
74
67
|
}
|
|
75
68
|
})
|
|
76
69
|
);
|
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
MAX_BATCH_CONCURRENCY,
|
|
14
14
|
METHOD_SET,
|
|
15
15
|
} from '../../protocol/src/index.js';
|
|
16
|
-
import { createBridgeClientForDestination } from '../../agent-client/src/remotes.js';
|
|
17
16
|
import {
|
|
18
17
|
annotateBridgeSummary,
|
|
19
18
|
applyLimitBudgetPreset,
|
|
@@ -31,6 +30,8 @@ import {
|
|
|
31
30
|
summarizeToolError,
|
|
32
31
|
summarizeToolResponse,
|
|
33
32
|
withToolClient,
|
|
33
|
+
withMcpRequestClient,
|
|
34
|
+
throwIfMcpRequestCancelled,
|
|
34
35
|
REQUEST_SOURCE,
|
|
35
36
|
} from './handlers-utils.js';
|
|
36
37
|
import { createScreenshotResult } from './handlers-capture.js';
|
|
@@ -383,33 +384,23 @@ export async function handleBatchTool(args) {
|
|
|
383
384
|
const tokenBudget = getToolTokenBudget(call);
|
|
384
385
|
const destinationId = typeof call.destinationId === 'string' ? call.destinationId : null;
|
|
385
386
|
const startTime = Date.now();
|
|
386
|
-
/** @type {BridgeClient | null} */
|
|
387
|
-
let callClient = null;
|
|
388
387
|
/** @type {import('../../protocol/src/types.js').BridgeResponse | null} */
|
|
389
388
|
let response = null;
|
|
390
389
|
/** @type {unknown} */
|
|
391
390
|
let callError = null;
|
|
392
391
|
try {
|
|
393
|
-
callClient = await createBridgeClientForDestination(destinationId, {
|
|
394
|
-
checkProtocolOnConnect: false,
|
|
395
|
-
});
|
|
396
|
-
await callClient.connect();
|
|
397
392
|
const params = applyMethodBudgetPreset(method, call.params || {}, call.budgetPreset);
|
|
398
|
-
response = await
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
393
|
+
response = await withMcpRequestClient(
|
|
394
|
+
(client) =>
|
|
395
|
+
requestBridgeWithRetry(client, method, params, {
|
|
396
|
+
tabId,
|
|
397
|
+
source: REQUEST_SOURCE,
|
|
398
|
+
tokenBudget,
|
|
399
|
+
}),
|
|
400
|
+
{ destinationId }
|
|
401
|
+
);
|
|
403
402
|
} catch (error) {
|
|
404
403
|
callError = error;
|
|
405
|
-
} finally {
|
|
406
|
-
if (callClient) {
|
|
407
|
-
try {
|
|
408
|
-
await callClient.close();
|
|
409
|
-
} catch (error) {
|
|
410
|
-
callError ??= error;
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
404
|
}
|
|
414
405
|
if (callError || !response) {
|
|
415
406
|
return {
|
|
@@ -489,6 +480,7 @@ async function mapWithConcurrency(values, concurrency, callback) {
|
|
|
489
480
|
let nextIndex = 0;
|
|
490
481
|
const worker = async () => {
|
|
491
482
|
while (nextIndex < values.length) {
|
|
483
|
+
throwIfMcpRequestCancelled();
|
|
492
484
|
const index = nextIndex;
|
|
493
485
|
nextIndex += 1;
|
|
494
486
|
results[index] = await callback(values[index], index);
|
|
@@ -537,6 +529,9 @@ export async function handleRawCallTool(args) {
|
|
|
537
529
|
if (method === 'sensitive.read') {
|
|
538
530
|
return createSensitiveReadResult(response);
|
|
539
531
|
}
|
|
532
|
+
if (method === 'artifact.read') {
|
|
533
|
+
return createArtifactReadResult(response);
|
|
534
|
+
}
|
|
540
535
|
return summarizeToolResponse(response, method, params);
|
|
541
536
|
},
|
|
542
537
|
{ destinationId: args.destinationId ?? null }
|
|
@@ -729,6 +724,7 @@ export async function handleInvestigateTool(args) {
|
|
|
729
724
|
const stepResults = [];
|
|
730
725
|
|
|
731
726
|
for (const step of scope.steps) {
|
|
727
|
+
throwIfMcpRequestCancelled();
|
|
732
728
|
const startTime = Date.now();
|
|
733
729
|
try {
|
|
734
730
|
const response = await requestBridgeWithRetry(client, step.method, step.params(args), {
|