@browserbridge/bbx 1.10.0 → 1.11.1
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/README.md +1 -1
- package/SECURITY.md +14 -2
- package/package.json +8 -10
- package/packages/agent-client/src/cli-proxy-remote.js +7 -4
- package/packages/agent-client/src/command-registry.js +1 -1
- package/packages/agent-client/src/npm-self-update.js +9 -2
- package/packages/agent-client/src/runtime.js +12 -3
- package/packages/agent-client/src/types.ts +2 -0
- package/packages/mcp-server/src/bin.js +1 -1
- package/packages/mcp-server/src/guidance.js +12 -6
- package/packages/mcp-server/src/handlers-utils.js +21 -4
- package/packages/mcp-server/src/handlers.js +10 -4
- package/packages/mcp-server/src/server.js +77 -62
- package/packages/mcp-server/src/toolset.js +25 -0
- package/packages/native-host/bin/postinstall.js +7 -5
- package/packages/native-host/src/auth-token.js +63 -1
- package/packages/native-host/src/daemon.js +148 -45
- package/packages/native-host/src/install-manifest.js +3 -9
- package/packages/native-host/src/native-host.js +3 -3
- package/packages/protocol/src/defaults.js +6 -0
- package/packages/protocol/src/protocol.js +72 -3
- package/packages/protocol/src/types.ts +4 -0
- package/skills/browser-bridge/SKILL.md +1 -1
|
@@ -44,7 +44,12 @@ import {
|
|
|
44
44
|
getSocketPath,
|
|
45
45
|
} from './config.js';
|
|
46
46
|
import { ArtifactStore } from './artifact-store.js';
|
|
47
|
-
import {
|
|
47
|
+
import {
|
|
48
|
+
bridgeAuthTokensEqual,
|
|
49
|
+
ensureBridgeAuthToken,
|
|
50
|
+
ensureBridgeExtensionAuthToken,
|
|
51
|
+
readBridgeAuthToken,
|
|
52
|
+
} from './auth-token.js';
|
|
48
53
|
import { normalizeDaemonLogger } from './daemon-logger.js';
|
|
49
54
|
import { DomBaselineOwnerRegistry, isValidBaselineId } from './dom-baseline-owners.js';
|
|
50
55
|
import { writeJsonLine } from './framing.js';
|
|
@@ -59,8 +64,8 @@ setProtocolPackageVersion(DAEMON_VERSION);
|
|
|
59
64
|
/** @typedef {import('./config.js').BridgeTransport} BridgeTransport */
|
|
60
65
|
/** @typedef {import('./daemon-logger.js').DaemonLoggerLike} DaemonLoggerLike */
|
|
61
66
|
/** @typedef {'agent' | 'extension'} SocketRole */
|
|
62
|
-
/** @typedef {import('node:net').Socket & { readonly __role?: SocketRole, __clientId?: string, __extensionId?: string, __browserName?: string, __profileLabel?: string, __browserExtensionId?: string, __accessEnabled?: boolean, __lastActiveAt?: number }} ClientSocket */
|
|
63
|
-
/** @typedef {{ socket: ClientSocket, timeoutId: NodeJS.Timeout, source?: string, method?: string, protocolVersion?: string, baselineId?: string | null, automaticMcpRetry?: boolean, targets: Set<ClientSocket>, lastErrorResponse?: import('../../protocol/src/types.js').BridgeResponse }} PendingEntry */
|
|
67
|
+
/** @typedef {import('node:net').Socket & { readonly __role?: SocketRole, __localControl?: boolean, __registrationTimeoutId?: NodeJS.Timeout, __invalidLineCount?: number, __clientId?: string, __extensionId?: string, __browserName?: string, __profileLabel?: string, __browserExtensionId?: string, __accessEnabled?: boolean, __lastActiveAt?: number }} ClientSocket */
|
|
68
|
+
/** @typedef {{ socket: ClientSocket, timeoutId: NodeJS.Timeout, source?: string, mcpEra?: string, method?: string, protocolVersion?: string, baselineId?: string | null, automaticMcpRetry?: boolean, targets: Set<ClientSocket>, lastErrorResponse?: import('../../protocol/src/types.js').BridgeResponse }} PendingEntry */
|
|
64
69
|
/**
|
|
65
70
|
* @typedef {{
|
|
66
71
|
* installAgentFiles: typeof import('../../agent-client/src/install.js').installAgentFiles,
|
|
@@ -123,6 +128,13 @@ const DIAGNOSTIC_METHODS = new Set([
|
|
|
123
128
|
'setup.get_status',
|
|
124
129
|
'setup.install',
|
|
125
130
|
]);
|
|
131
|
+
export const DAEMON_REGISTRATION_TIMEOUT_MS = 5_000;
|
|
132
|
+
export const MAX_DAEMON_CONNECTIONS = 64;
|
|
133
|
+
export const MAX_DAEMON_PENDING_REQUESTS = 128;
|
|
134
|
+
export const MAX_DAEMON_PENDING_REQUESTS_PER_CLIENT = 16;
|
|
135
|
+
export const MAX_DAEMON_INVALID_LINES = 3;
|
|
136
|
+
const MAX_CLIENT_ID_LENGTH = 128;
|
|
137
|
+
const MAX_ROUTING_LABEL_LENGTH = 128;
|
|
126
138
|
|
|
127
139
|
/**
|
|
128
140
|
* @param {unknown} value
|
|
@@ -134,6 +146,42 @@ function asHealthRecord(value) {
|
|
|
134
146
|
: null;
|
|
135
147
|
}
|
|
136
148
|
|
|
149
|
+
/**
|
|
150
|
+
* @param {unknown} value
|
|
151
|
+
* @returns {string | undefined}
|
|
152
|
+
*/
|
|
153
|
+
function normalizeRoutingLabel(value) {
|
|
154
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > MAX_ROUTING_LABEL_LENGTH) {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
return hasAsciiControlCharacters(value) ? undefined : value;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** @param {string} value @returns {boolean} */
|
|
161
|
+
function hasAsciiControlCharacters(value) {
|
|
162
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
163
|
+
const code = value.charCodeAt(index);
|
|
164
|
+
if (code < 32 || code === 127) return true;
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* @param {unknown} value
|
|
171
|
+
* @returns {string | null}
|
|
172
|
+
*/
|
|
173
|
+
function normalizeClientId(value) {
|
|
174
|
+
if (value == null || value === '') return randomUUID();
|
|
175
|
+
if (
|
|
176
|
+
typeof value !== 'string' ||
|
|
177
|
+
value.length > MAX_CLIENT_ID_LENGTH ||
|
|
178
|
+
!/^[A-Za-z0-9_.:-]+$/u.test(value)
|
|
179
|
+
) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
return value;
|
|
183
|
+
}
|
|
184
|
+
|
|
137
185
|
/**
|
|
138
186
|
* @param {unknown} value
|
|
139
187
|
* @returns {number | undefined}
|
|
@@ -383,6 +431,7 @@ export class BridgeDaemon {
|
|
|
383
431
|
* setupInstaller?: (params: Record<string, unknown>) => Promise<SetupInstallResult>,
|
|
384
432
|
* logger?: DaemonLoggerLike | Pick<Console, 'log' | 'error'>,
|
|
385
433
|
* authToken?: string | null,
|
|
434
|
+
* extensionAuthToken?: string | null,
|
|
386
435
|
* artifactStore?: ArtifactStore
|
|
387
436
|
* }} [options={}]
|
|
388
437
|
*/
|
|
@@ -394,6 +443,7 @@ export class BridgeDaemon {
|
|
|
394
443
|
setupInstaller = installSetupTarget,
|
|
395
444
|
logger = undefined,
|
|
396
445
|
authToken = undefined,
|
|
446
|
+
extensionAuthToken = undefined,
|
|
397
447
|
artifactStore = new ArtifactStore(getArtifactStorePath()),
|
|
398
448
|
} = {}) {
|
|
399
449
|
this.transport = socketPath ? createSocketBridgeTransport(socketPath) : transport;
|
|
@@ -437,6 +487,8 @@ export class BridgeDaemon {
|
|
|
437
487
|
this.requestStartTimes = new Map();
|
|
438
488
|
/** @type {string | null | undefined} */
|
|
439
489
|
this.authToken = authToken;
|
|
490
|
+
/** @type {string | null | undefined} */
|
|
491
|
+
this.extensionAuthToken = extensionAuthToken;
|
|
440
492
|
this.artifactStore = artifactStore;
|
|
441
493
|
this.artifactStoreInitialized = false;
|
|
442
494
|
this.domBaselines = new DomBaselineOwnerRegistry({
|
|
@@ -580,26 +632,32 @@ export class BridgeDaemon {
|
|
|
580
632
|
return;
|
|
581
633
|
}
|
|
582
634
|
|
|
583
|
-
if (
|
|
584
|
-
this.logger.error('socket registration rejected', { role: message.role ?? null });
|
|
635
|
+
if (message.role !== 'extension' && message.role !== 'agent') {
|
|
585
636
|
void writeJsonLine(socket, {
|
|
586
637
|
type: 'registration_failed',
|
|
587
638
|
error: {
|
|
588
|
-
code: ERROR_CODES.
|
|
589
|
-
message: '
|
|
639
|
+
code: ERROR_CODES.INVALID_REQUEST,
|
|
640
|
+
message: 'Socket role must be "agent" or "extension".',
|
|
590
641
|
},
|
|
591
|
-
})
|
|
642
|
+
});
|
|
592
643
|
return;
|
|
593
644
|
}
|
|
594
645
|
|
|
595
|
-
|
|
646
|
+
const expectedToken =
|
|
647
|
+
message.role === 'extension'
|
|
648
|
+
? this.extensionAuthToken
|
|
649
|
+
: this.isAuthRequired()
|
|
650
|
+
? this.authToken
|
|
651
|
+
: null;
|
|
652
|
+
if (expectedToken && !bridgeAuthTokensEqual(message.authToken, expectedToken)) {
|
|
653
|
+
this.logger.error('socket registration rejected', { role: message.role ?? null });
|
|
596
654
|
void writeJsonLine(socket, {
|
|
597
655
|
type: 'registration_failed',
|
|
598
656
|
error: {
|
|
599
|
-
code: ERROR_CODES.
|
|
600
|
-
message: '
|
|
657
|
+
code: ERROR_CODES.ACCESS_DENIED,
|
|
658
|
+
message: 'Bridge daemon authentication failed.',
|
|
601
659
|
},
|
|
602
|
-
});
|
|
660
|
+
}).finally(() => socket.destroy());
|
|
603
661
|
return;
|
|
604
662
|
}
|
|
605
663
|
|
|
@@ -609,13 +667,16 @@ export class BridgeDaemon {
|
|
|
609
667
|
configurable: false,
|
|
610
668
|
writable: false,
|
|
611
669
|
});
|
|
670
|
+
if (socket.__registrationTimeoutId) {
|
|
671
|
+
clearTimeout(socket.__registrationTimeoutId);
|
|
672
|
+
socket.__registrationTimeoutId = undefined;
|
|
673
|
+
}
|
|
612
674
|
if (message.role === 'extension') {
|
|
675
|
+
socket.__localControl = true;
|
|
613
676
|
const extensionId = randomUUID();
|
|
614
677
|
socket.__extensionId = extensionId;
|
|
615
|
-
socket.__browserName =
|
|
616
|
-
|
|
617
|
-
socket.__profileLabel =
|
|
618
|
-
typeof message.profileLabel === 'string' ? message.profileLabel : undefined;
|
|
678
|
+
socket.__browserName = normalizeRoutingLabel(message.browserName);
|
|
679
|
+
socket.__profileLabel = normalizeRoutingLabel(message.profileLabel);
|
|
619
680
|
socket.__browserExtensionId = normalizeBrowserExtensionId(message.browserExtensionId);
|
|
620
681
|
socket.__lastActiveAt = Date.now();
|
|
621
682
|
this.extensionSockets.set(extensionId, socket);
|
|
@@ -630,7 +691,18 @@ export class BridgeDaemon {
|
|
|
630
691
|
}
|
|
631
692
|
|
|
632
693
|
if (message.role === 'agent') {
|
|
633
|
-
|
|
694
|
+
socket.__localControl = this.transport.type === 'socket';
|
|
695
|
+
const clientId = normalizeClientId(message.clientId);
|
|
696
|
+
if (!clientId) {
|
|
697
|
+
void writeJsonLine(socket, {
|
|
698
|
+
type: 'registration_failed',
|
|
699
|
+
error: {
|
|
700
|
+
code: ERROR_CODES.INVALID_REQUEST,
|
|
701
|
+
message: 'Agent clientId is invalid.',
|
|
702
|
+
},
|
|
703
|
+
}).finally(() => socket.destroy());
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
634
706
|
this.agentSockets.set(clientId, socket);
|
|
635
707
|
socket.__clientId = clientId;
|
|
636
708
|
this.logger.info('agent registered', { clientId });
|
|
@@ -649,6 +721,10 @@ export class BridgeDaemon {
|
|
|
649
721
|
if (this.authToken === undefined) {
|
|
650
722
|
this.authToken = this.transport.type === 'tcp' ? await ensureBridgeAuthToken() : null;
|
|
651
723
|
}
|
|
724
|
+
if (this.extensionAuthToken === undefined) {
|
|
725
|
+
this.extensionAuthToken =
|
|
726
|
+
this.transport.type === 'tcp' ? await ensureBridgeExtensionAuthToken() : null;
|
|
727
|
+
}
|
|
652
728
|
|
|
653
729
|
if (this.transport.type === 'socket' && !isWindowsNamedPipePath(this.socketPath)) {
|
|
654
730
|
const socketDir = path.dirname(this.socketPath);
|
|
@@ -677,6 +753,14 @@ export class BridgeDaemon {
|
|
|
677
753
|
|
|
678
754
|
this.server = net.createServer((socket) => {
|
|
679
755
|
const typedSocket = /** @type {ClientSocket} */ (socket);
|
|
756
|
+
typedSocket.__invalidLineCount = 0;
|
|
757
|
+
typedSocket.__registrationTimeoutId = setTimeout(() => {
|
|
758
|
+
if (!typedSocket.__role) {
|
|
759
|
+
this.logger.warn('unregistered socket timed out');
|
|
760
|
+
typedSocket.destroy();
|
|
761
|
+
}
|
|
762
|
+
}, DAEMON_REGISTRATION_TIMEOUT_MS);
|
|
763
|
+
typedSocket.__registrationTimeoutId.unref?.();
|
|
680
764
|
typedSocket.on('error', (err) => {
|
|
681
765
|
this.logger.error('socket error', { message: err.message });
|
|
682
766
|
});
|
|
@@ -694,10 +778,18 @@ export class BridgeDaemon {
|
|
|
694
778
|
onProtocolError: (error) => {
|
|
695
779
|
this.logger.error('socket protocol error', { message: error.message });
|
|
696
780
|
},
|
|
781
|
+
onInvalidLine: () => {
|
|
782
|
+
typedSocket.__invalidLineCount = (typedSocket.__invalidLineCount ?? 0) + 1;
|
|
783
|
+
if (typedSocket.__invalidLineCount >= MAX_DAEMON_INVALID_LINES) {
|
|
784
|
+
this.logger.warn('socket sent too many malformed JSON lines');
|
|
785
|
+
typedSocket.destroy();
|
|
786
|
+
}
|
|
787
|
+
},
|
|
697
788
|
}
|
|
698
789
|
);
|
|
699
790
|
typedSocket.on('close', () => this.handleSocketClose(typedSocket));
|
|
700
791
|
});
|
|
792
|
+
this.server.maxConnections = MAX_DAEMON_CONNECTIONS;
|
|
701
793
|
|
|
702
794
|
const server = this.server;
|
|
703
795
|
await new Promise((resolve, reject) => {
|
|
@@ -1076,7 +1168,7 @@ export class BridgeDaemon {
|
|
|
1076
1168
|
}
|
|
1077
1169
|
|
|
1078
1170
|
if (request.method === 'setup.install') {
|
|
1079
|
-
if (socket.
|
|
1171
|
+
if (!socket.__localControl) {
|
|
1080
1172
|
const response = createFailure(
|
|
1081
1173
|
request.id,
|
|
1082
1174
|
ERROR_CODES.ACCESS_DENIED,
|
|
@@ -1115,6 +1207,26 @@ export class BridgeDaemon {
|
|
|
1115
1207
|
typeof request.meta?.target_profile === 'string' ? request.meta.target_profile : null;
|
|
1116
1208
|
const hasExplicitTarget = Boolean(targetBrowser || targetProfile);
|
|
1117
1209
|
|
|
1210
|
+
const ownerPendingCount = this.pendingRequestsByOwnerSocket.get(socket)?.size ?? 0;
|
|
1211
|
+
if (
|
|
1212
|
+
this.pendingRequests.size >= MAX_DAEMON_PENDING_REQUESTS ||
|
|
1213
|
+
ownerPendingCount >= MAX_DAEMON_PENDING_REQUESTS_PER_CLIENT
|
|
1214
|
+
) {
|
|
1215
|
+
const response = createFailure(
|
|
1216
|
+
request.id,
|
|
1217
|
+
ERROR_CODES.INVALID_REQUEST,
|
|
1218
|
+
'Daemon pending-request limit reached. Wait for existing requests to finish.',
|
|
1219
|
+
{
|
|
1220
|
+
maxPendingRequests: MAX_DAEMON_PENDING_REQUESTS,
|
|
1221
|
+
maxPendingRequestsPerClient: MAX_DAEMON_PENDING_REQUESTS_PER_CLIENT,
|
|
1222
|
+
},
|
|
1223
|
+
{ method: request.method }
|
|
1224
|
+
);
|
|
1225
|
+
await writeJsonLine(socket, { type: 'agent.response', response });
|
|
1226
|
+
this.recordDaemonRequestOutcome(request, false);
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1118
1230
|
const baselineId =
|
|
1119
1231
|
typeof request.params.baselineId === 'string' ? request.params.baselineId : null;
|
|
1120
1232
|
const usesBaselineOwner =
|
|
@@ -1165,6 +1277,7 @@ export class BridgeDaemon {
|
|
|
1165
1277
|
method: request.method,
|
|
1166
1278
|
protocolVersion: request.meta?.protocol_version,
|
|
1167
1279
|
source: typeof request.meta?.source === 'string' ? request.meta.source : '',
|
|
1280
|
+
mcpEra: typeof request.meta?.mcp_era === 'string' ? request.meta.mcp_era : '',
|
|
1168
1281
|
baselineId,
|
|
1169
1282
|
automaticMcpRetry: isAutomaticMcpRetry(request.meta),
|
|
1170
1283
|
targets: new Set([target]),
|
|
@@ -1221,23 +1334,6 @@ export class BridgeDaemon {
|
|
|
1221
1334
|
}
|
|
1222
1335
|
}
|
|
1223
1336
|
|
|
1224
|
-
/**
|
|
1225
|
-
* @param {ClientSocket} socket
|
|
1226
|
-
* @returns {boolean}
|
|
1227
|
-
*/
|
|
1228
|
-
isLocalSocket(socket) {
|
|
1229
|
-
if (this.transport.type === 'socket') {
|
|
1230
|
-
return true;
|
|
1231
|
-
}
|
|
1232
|
-
const address = socket.remoteAddress ?? '';
|
|
1233
|
-
return (
|
|
1234
|
-
address === '127.0.0.1' ||
|
|
1235
|
-
address === '::1' ||
|
|
1236
|
-
address === 'localhost' ||
|
|
1237
|
-
address === '::ffff:127.0.0.1'
|
|
1238
|
-
);
|
|
1239
|
-
}
|
|
1240
|
-
|
|
1241
1337
|
/**
|
|
1242
1338
|
* Select from a snapshot so routing neither mutates nor observes mutations to
|
|
1243
1339
|
* the live extension map while candidates are ordered.
|
|
@@ -1347,13 +1443,15 @@ export class BridgeDaemon {
|
|
|
1347
1443
|
*/
|
|
1348
1444
|
handleExtensionIdentity(socket, message) {
|
|
1349
1445
|
let changed = false;
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
socket.__browserName
|
|
1446
|
+
const browserName = normalizeRoutingLabel(message.browserName);
|
|
1447
|
+
if (browserName) {
|
|
1448
|
+
changed = changed || socket.__browserName !== browserName;
|
|
1449
|
+
socket.__browserName = browserName;
|
|
1353
1450
|
}
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
socket.__profileLabel
|
|
1451
|
+
const profileLabel = normalizeRoutingLabel(message.profileLabel);
|
|
1452
|
+
if (profileLabel) {
|
|
1453
|
+
changed = changed || socket.__profileLabel !== profileLabel;
|
|
1454
|
+
socket.__profileLabel = profileLabel;
|
|
1357
1455
|
}
|
|
1358
1456
|
if (typeof message.browserExtensionId === 'string') {
|
|
1359
1457
|
const browserExtensionId = normalizeBrowserExtensionId(message.browserExtensionId);
|
|
@@ -1456,12 +1554,11 @@ export class BridgeDaemon {
|
|
|
1456
1554
|
|
|
1457
1555
|
/**
|
|
1458
1556
|
* @param {ClientSocket} socket
|
|
1459
|
-
* @param {DaemonMessage}
|
|
1557
|
+
* @param {DaemonMessage} _message
|
|
1460
1558
|
* @returns {void}
|
|
1461
1559
|
*/
|
|
1462
|
-
handleExtensionActivity(socket,
|
|
1463
|
-
socket.__lastActiveAt =
|
|
1464
|
-
typeof message.at === 'number' && Number.isFinite(message.at) ? message.at : Date.now();
|
|
1560
|
+
handleExtensionActivity(socket, _message) {
|
|
1561
|
+
socket.__lastActiveAt = Date.now();
|
|
1465
1562
|
}
|
|
1466
1563
|
|
|
1467
1564
|
/**
|
|
@@ -1603,6 +1700,7 @@ export class BridgeDaemon {
|
|
|
1603
1700
|
ok: true,
|
|
1604
1701
|
id: responseMessage.id,
|
|
1605
1702
|
source: pending.source || null,
|
|
1703
|
+
...(pending.mcpEra ? { mcpEra: pending.mcpEra } : {}),
|
|
1606
1704
|
});
|
|
1607
1705
|
return;
|
|
1608
1706
|
}
|
|
@@ -1621,6 +1719,10 @@ export class BridgeDaemon {
|
|
|
1621
1719
|
* @returns {void}
|
|
1622
1720
|
*/
|
|
1623
1721
|
handleSocketClose(socket) {
|
|
1722
|
+
if (socket.__registrationTimeoutId) {
|
|
1723
|
+
clearTimeout(socket.__registrationTimeoutId);
|
|
1724
|
+
socket.__registrationTimeoutId = undefined;
|
|
1725
|
+
}
|
|
1624
1726
|
if (socket.__extensionId) {
|
|
1625
1727
|
this.artifactStore.deleteByExtension(socket.__extensionId);
|
|
1626
1728
|
this.domBaselines.clearForSocket(socket);
|
|
@@ -1702,6 +1804,7 @@ export class BridgeDaemon {
|
|
|
1702
1804
|
ok: false,
|
|
1703
1805
|
id: requestId,
|
|
1704
1806
|
source: pending.source || null,
|
|
1807
|
+
...(pending.mcpEra ? { mcpEra: pending.mcpEra } : {}),
|
|
1705
1808
|
});
|
|
1706
1809
|
|
|
1707
1810
|
await writeJsonLine(pending.socket, {
|
|
@@ -184,7 +184,8 @@ export function getDefaultExtensionId(env = process.env) {
|
|
|
184
184
|
|
|
185
185
|
/**
|
|
186
186
|
* Build the allowed_origins list.
|
|
187
|
-
* If an extension ID was provided,
|
|
187
|
+
* If an extension ID was provided, it is the complete allowlist so stale
|
|
188
|
+
* origins from an earlier installation do not retain native-host access.
|
|
188
189
|
* Otherwise falls back to existing origins or a placeholder.
|
|
189
190
|
*
|
|
190
191
|
* @param {{allowed_origins?: string[]} | null} existingManifest
|
|
@@ -199,14 +200,7 @@ export function getAllowedOrigins(existingManifest, extensionId) {
|
|
|
199
200
|
|
|
200
201
|
if (extensionId) {
|
|
201
202
|
const origin = `chrome-extension://${extensionId}/`;
|
|
202
|
-
|
|
203
|
-
merged.add(origin);
|
|
204
|
-
for (const item of [...merged]) {
|
|
205
|
-
if (item.includes('__REPLACE_WITH_EXTENSION_ID__')) {
|
|
206
|
-
merged.delete(item);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
return [...merged];
|
|
203
|
+
return [origin];
|
|
210
204
|
}
|
|
211
205
|
|
|
212
206
|
if (existing.length > 0) return existing;
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
parseJsonLines,
|
|
9
9
|
sanitizeIncidentalText,
|
|
10
10
|
} from '../../protocol/src/index.js';
|
|
11
|
-
import {
|
|
11
|
+
import { readBridgeExtensionAuthToken } from './auth-token.js';
|
|
12
12
|
import { createSocketBridgeTransport, getBridgeTransport } from './config.js';
|
|
13
13
|
import { spawnBridgeDaemonProcess } from './daemon-process.js';
|
|
14
14
|
import { createNativeMessageReader, createNativeMessageWriter, writeJsonLine } from './framing.js';
|
|
@@ -163,11 +163,11 @@ export async function runNativeHost({
|
|
|
163
163
|
socket.once('close', cleanupStdinEndListener);
|
|
164
164
|
socket.once('end', cleanupStdinEndListener);
|
|
165
165
|
socket.once('error', cleanupStdinEndListener);
|
|
166
|
-
const authToken =
|
|
166
|
+
const authToken = await readBridgeExtensionAuthToken();
|
|
167
167
|
await writeJsonLine(socket, {
|
|
168
168
|
type: 'register',
|
|
169
169
|
role: 'extension',
|
|
170
|
-
|
|
170
|
+
authToken,
|
|
171
171
|
});
|
|
172
172
|
|
|
173
173
|
parseJsonLines(
|
|
@@ -45,6 +45,12 @@ export const MAX_BATCH_CALLS = 20;
|
|
|
45
45
|
|
|
46
46
|
/** Maximum read-only batch calls executing concurrently. */
|
|
47
47
|
export const MAX_BATCH_CONCURRENCY = 5;
|
|
48
|
+
export const MAX_INTERCEPT_RULES_PER_TAB = 32;
|
|
49
|
+
export const MAX_INTERCEPT_URL_PATTERN_LENGTH = 2_048;
|
|
50
|
+
export const MAX_INTERCEPT_BODY_BYTES = 262_144;
|
|
51
|
+
export const MAX_INTERCEPT_HEADERS = 64;
|
|
52
|
+
export const MAX_INTERCEPT_HEADER_VALUE_BYTES = 8_192;
|
|
53
|
+
export const MAX_INTERCEPT_HEADER_BYTES = 65_536;
|
|
48
54
|
export const SCREENSHOT_AUTO_INLINE_BYTES = 262_144;
|
|
49
55
|
export const SCREENSHOT_MAX_INLINE_BYTES = 524_288;
|
|
50
56
|
export const ARTIFACT_CHUNK_BYTES = 196_608;
|
|
@@ -26,6 +26,11 @@ import {
|
|
|
26
26
|
MAX_EXTRACT_SETTLE_TIMEOUT_MS,
|
|
27
27
|
MAX_ARTIFACT_BYTES,
|
|
28
28
|
MAX_HAR_ENTRIES,
|
|
29
|
+
MAX_INTERCEPT_BODY_BYTES,
|
|
30
|
+
MAX_INTERCEPT_HEADER_BYTES,
|
|
31
|
+
MAX_INTERCEPT_HEADERS,
|
|
32
|
+
MAX_INTERCEPT_HEADER_VALUE_BYTES,
|
|
33
|
+
MAX_INTERCEPT_URL_PATTERN_LENGTH,
|
|
29
34
|
MAX_SENSITIVE_VALUE_BYTES,
|
|
30
35
|
} from './defaults.js';
|
|
31
36
|
import { BridgeError, ERROR_CODES, getErrorRecovery } from './errors.js';
|
|
@@ -300,8 +305,16 @@ export function validateBridgeRequest(request) {
|
|
|
300
305
|
|
|
301
306
|
const candidate = /** @type {Record<string, unknown>} */ (request);
|
|
302
307
|
|
|
303
|
-
if (
|
|
304
|
-
|
|
308
|
+
if (
|
|
309
|
+
typeof candidate.id !== 'string' ||
|
|
310
|
+
!candidate.id.trim() ||
|
|
311
|
+
candidate.id.length > 128 ||
|
|
312
|
+
hasAsciiControlCharacters(candidate.id)
|
|
313
|
+
) {
|
|
314
|
+
throw new BridgeError(
|
|
315
|
+
ERROR_CODES.INVALID_REQUEST,
|
|
316
|
+
'Request id must be a non-empty string of at most 128 characters without controls.'
|
|
317
|
+
);
|
|
305
318
|
}
|
|
306
319
|
|
|
307
320
|
if (
|
|
@@ -372,11 +385,30 @@ export function validateBridgeRequest(request) {
|
|
|
372
385
|
typeof meta.protocol_version === 'string' ? meta.protocol_version : getProtocolVersion(),
|
|
373
386
|
token_budget: typeof meta.token_budget === 'number' ? meta.token_budget : null,
|
|
374
387
|
source: meta.source === 'cli' || meta.source === 'mcp' ? meta.source : undefined,
|
|
388
|
+
mcp_era: normalizeMcpEra(meta.source, meta.mcp_era),
|
|
375
389
|
automatic_retry: normalizeAutomaticRetryMeta(meta.source, meta.automatic_retry),
|
|
376
390
|
},
|
|
377
391
|
};
|
|
378
392
|
}
|
|
379
393
|
|
|
394
|
+
/** @param {string} value @returns {boolean} */
|
|
395
|
+
function hasAsciiControlCharacters(value) {
|
|
396
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
397
|
+
const code = value.charCodeAt(index);
|
|
398
|
+
if (code < 32 || code === 127) return true;
|
|
399
|
+
}
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* @param {unknown} source
|
|
405
|
+
* @param {unknown} value
|
|
406
|
+
* @returns {import('./types.js').McpProtocolEra | undefined}
|
|
407
|
+
*/
|
|
408
|
+
function normalizeMcpEra(source, value) {
|
|
409
|
+
return source === 'mcp' && (value === 'legacy' || value === 'modern') ? value : undefined;
|
|
410
|
+
}
|
|
411
|
+
|
|
380
412
|
/**
|
|
381
413
|
* @param {unknown} source
|
|
382
414
|
* @param {unknown} value
|
|
@@ -1493,6 +1525,12 @@ export function normalizeNetworkInterceptAddParams(params = {}) {
|
|
|
1493
1525
|
if (typeof params.urlPattern !== 'string' || !params.urlPattern) {
|
|
1494
1526
|
throw new BridgeError(ERROR_CODES.INVALID_REQUEST, 'urlPattern is required.');
|
|
1495
1527
|
}
|
|
1528
|
+
if (params.urlPattern.length > MAX_INTERCEPT_URL_PATTERN_LENGTH) {
|
|
1529
|
+
throw new BridgeError(
|
|
1530
|
+
ERROR_CODES.INVALID_REQUEST,
|
|
1531
|
+
`urlPattern must not exceed ${MAX_INTERCEPT_URL_PATTERN_LENGTH} characters.`
|
|
1532
|
+
);
|
|
1533
|
+
}
|
|
1496
1534
|
|
|
1497
1535
|
const action = params.action ?? DEFAULT_NETWORK_INTERCEPT_ACTION;
|
|
1498
1536
|
if (action !== 'fulfill' && action !== 'continue' && action !== 'block') {
|
|
@@ -1513,6 +1551,15 @@ export function normalizeNetworkInterceptAddParams(params = {}) {
|
|
|
1513
1551
|
if (params.body !== undefined && typeof params.body !== 'string') {
|
|
1514
1552
|
throw new BridgeError(ERROR_CODES.INVALID_REQUEST, 'body must be a string.');
|
|
1515
1553
|
}
|
|
1554
|
+
if (
|
|
1555
|
+
typeof params.body === 'string' &&
|
|
1556
|
+
new TextEncoder().encode(params.body).byteLength > MAX_INTERCEPT_BODY_BYTES
|
|
1557
|
+
) {
|
|
1558
|
+
throw new BridgeError(
|
|
1559
|
+
ERROR_CODES.INVALID_REQUEST,
|
|
1560
|
+
`body must not exceed ${MAX_INTERCEPT_BODY_BYTES} UTF-8 bytes.`
|
|
1561
|
+
);
|
|
1562
|
+
}
|
|
1516
1563
|
|
|
1517
1564
|
/** @type {Record<string, string> | undefined} */
|
|
1518
1565
|
let headers;
|
|
@@ -1523,8 +1570,16 @@ export function normalizeNetworkInterceptAddParams(params = {}) {
|
|
|
1523
1570
|
'headers must be an object containing string values.'
|
|
1524
1571
|
);
|
|
1525
1572
|
}
|
|
1573
|
+
const entries = Object.entries(params.headers);
|
|
1574
|
+
if (entries.length > MAX_INTERCEPT_HEADERS) {
|
|
1575
|
+
throw new BridgeError(
|
|
1576
|
+
ERROR_CODES.INVALID_REQUEST,
|
|
1577
|
+
`headers must contain at most ${MAX_INTERCEPT_HEADERS} entries.`
|
|
1578
|
+
);
|
|
1579
|
+
}
|
|
1526
1580
|
headers = {};
|
|
1527
|
-
|
|
1581
|
+
let headerBytes = 0;
|
|
1582
|
+
for (const [name, value] of entries) {
|
|
1528
1583
|
if (!/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(name)) {
|
|
1529
1584
|
throw new BridgeError(
|
|
1530
1585
|
ERROR_CODES.INVALID_REQUEST,
|
|
@@ -1537,6 +1592,20 @@ export function normalizeNetworkInterceptAddParams(params = {}) {
|
|
|
1537
1592
|
`Header ${name} must have a string value without newlines.`
|
|
1538
1593
|
);
|
|
1539
1594
|
}
|
|
1595
|
+
const valueBytes = new TextEncoder().encode(value).byteLength;
|
|
1596
|
+
if (valueBytes > MAX_INTERCEPT_HEADER_VALUE_BYTES) {
|
|
1597
|
+
throw new BridgeError(
|
|
1598
|
+
ERROR_CODES.INVALID_REQUEST,
|
|
1599
|
+
`Header ${name} must not exceed ${MAX_INTERCEPT_HEADER_VALUE_BYTES} UTF-8 bytes.`
|
|
1600
|
+
);
|
|
1601
|
+
}
|
|
1602
|
+
headerBytes += new TextEncoder().encode(name).byteLength + valueBytes;
|
|
1603
|
+
if (headerBytes > MAX_INTERCEPT_HEADER_BYTES) {
|
|
1604
|
+
throw new BridgeError(
|
|
1605
|
+
ERROR_CODES.INVALID_REQUEST,
|
|
1606
|
+
`headers must not exceed ${MAX_INTERCEPT_HEADER_BYTES} UTF-8 bytes in total.`
|
|
1607
|
+
);
|
|
1608
|
+
}
|
|
1540
1609
|
headers[name] = value;
|
|
1541
1610
|
}
|
|
1542
1611
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export type BridgeRequestSource = 'cli' | 'mcp';
|
|
2
|
+
export type McpProtocolEra = 'legacy' | 'modern';
|
|
2
3
|
|
|
3
4
|
export type AccessIntent = 'inspect' | 'interact' | 'capture' | 'navigate' | 'debugger' | 'general';
|
|
4
5
|
|
|
@@ -153,6 +154,7 @@ export interface BridgeMeta {
|
|
|
153
154
|
image_approx_tokens?: number;
|
|
154
155
|
image_bytes?: number;
|
|
155
156
|
source?: BridgeRequestSource;
|
|
157
|
+
mcp_era?: McpProtocolEra;
|
|
156
158
|
response_bytes?: number;
|
|
157
159
|
approx_tokens?: number;
|
|
158
160
|
cost_class?: CostClass;
|
|
@@ -1051,6 +1053,8 @@ export interface NetworkResult {
|
|
|
1051
1053
|
dropped: number;
|
|
1052
1054
|
abandoned: number;
|
|
1053
1055
|
source: NetworkSource;
|
|
1056
|
+
provenance: 'page_main_world' | 'chrome_devtools_protocol';
|
|
1057
|
+
integrity: 'untrusted' | 'browser_observed';
|
|
1054
1058
|
capture: NetworkCaptureAction | null;
|
|
1055
1059
|
armed: boolean;
|
|
1056
1060
|
armedDuringCapture: boolean;
|
|
@@ -15,7 +15,7 @@ Skill name: `browser-bridge` (also known as `bbx`). In GitHub Copilot, invoke as
|
|
|
15
15
|
When the runtime supports subagents, delegate bridge inspection to a smaller, lower-cost worker and return only concise findings to the parent.
|
|
16
16
|
For open-ended investigation, start with structured reads (`page.get_state`, `dom.query`, `page.get_text`, `page.extract_content`, `styles.get_computed`, and `bbx batch` for CLI or `browser_batch` for MCP) and escalate to screenshots or debugger-backed methods only when structured evidence is insufficient. `browser_call` accepts one protocol method at a time; `batch` is not a valid `browser_call` method.
|
|
17
17
|
When an unfamiliar method is needed, run `bbx protocol describe <method|group>` before guessing parameters. In MCP mode, call `browser_call` with method `protocol.describe` and params `{ "method": "dom.query" }` or `{ "group": "inspect" }`.
|
|
18
|
-
MCP exposes
|
|
18
|
+
MCP exposes a compact tool surface. If `browser_toolset` is available on a legacy connection, load one specialized typed tool only when needed, for example `{ "tool": "browser_dom" }`. Modern stateless connections use a fixed surface with `browser_skill` instead. `browser_call` remains the universal path in both eras.
|
|
19
19
|
|
|
20
20
|
## CLI
|
|
21
21
|
|