@browserbridge/bbx 1.11.5 → 1.12.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/PRIVACY.md +1 -0
- package/package.json +3 -1
- package/packages/agent-client/src/client.js +18 -1
- package/packages/agent-client/src/types.ts +2 -0
- package/packages/mcp-server/src/handlers-capture.js +17 -7
- package/packages/mcp-server/src/handlers-dom.js +6 -1
- package/packages/mcp-server/src/handlers-utils.js +3 -0
- package/packages/mcp-server/src/server.js +49 -2
- package/packages/native-host/src/daemon.js +78 -4
- package/packages/native-host/src/native-host.js +40 -2
- package/packages/native-host/src/window-actions.js +115 -0
- package/packages/protocol/src/protocol.js +146 -15
- package/packages/protocol/src/registry.js +8 -6
- package/packages/protocol/src/summary.js +184 -32
- package/packages/protocol/src/types.ts +58 -7
- package/packages/protocol/src/window-access.ts +29 -0
- package/packages/protocol/src/window-actions.js +46 -0
- package/skills/browser-bridge/SKILL.md +18 -12
- package/skills/browser-bridge/references/interaction.md +12 -16
- package/skills/browser-bridge/references/protocol.md +16 -6
- package/skills/browser-bridge/references/token-efficiency.md +12 -6
package/PRIVACY.md
CHANGED
|
@@ -82,6 +82,7 @@ You should review the privacy and retention practices of any connected agent, ID
|
|
|
82
82
|
Browser Bridge keeps data only as long as needed for the current local session and feature behavior.
|
|
83
83
|
|
|
84
84
|
- Window enablement state is stored in session storage and cleared when disabled or when the browser session ends.
|
|
85
|
+
- To warn about access enabled elsewhere, the daemon shares enabled-window IDs, bounded titles, enablement timestamps, browser names, and session profile labels with connected local Browser Bridge extensions. The side panel uses this in-memory metadata for its other-enabled-windows counter. It does not include page contents or disabled-window details, and disconnecting an extension removes its entry.
|
|
85
86
|
- Action log entries are stored for the current browser session only.
|
|
86
87
|
- Incidental action/log URLs remove credentials and fragments and redact query values; sensitive structured fields and local path prefixes are reduced before persistence.
|
|
87
88
|
- Console and fetch/XHR network buffers are bounded in memory and cleared when disabled or when the page state is reset.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@browserbridge/bbx",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent-tools",
|
|
@@ -56,6 +56,8 @@
|
|
|
56
56
|
"format": "oxfmt .",
|
|
57
57
|
"format:check": "oxfmt --check .",
|
|
58
58
|
"fixture:browser": "node tests/fixtures/browser-reliability/server.mjs",
|
|
59
|
+
"fixture:interaction": "node tests/fixtures/agent-interaction/server.mjs",
|
|
60
|
+
"bench:interaction": "node tests/fixtures/agent-interaction/bench.mjs",
|
|
59
61
|
"test:runtime": "node --import tsx --test packages/*/test/*.test.ts packages/integration-tests/*.test.ts tests/fixtures/*/*.test.ts",
|
|
60
62
|
"test": "c8 node --import tsx --test packages/*/test/*.test.ts packages/integration-tests/*.test.ts tests/fixtures/*/*.test.ts",
|
|
61
63
|
"coverage": "c8 report --reporter=text --reporter=text-summary",
|
|
@@ -139,6 +139,18 @@ async function writeSocketLine(socket, line, signal) {
|
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Agent session that keeps tab routing sticky across separate CLI processes.
|
|
144
|
+
* Set `BBX_SESSION` to give one agent its own working tab; unset callers share
|
|
145
|
+
* the extension's default session.
|
|
146
|
+
*
|
|
147
|
+
* @returns {string | null}
|
|
148
|
+
*/
|
|
149
|
+
export function getDefaultAgentSession() {
|
|
150
|
+
const value = process.env.BBX_SESSION?.trim();
|
|
151
|
+
return value ? value : null;
|
|
152
|
+
}
|
|
153
|
+
|
|
142
154
|
export class BridgeClient extends EventEmitter {
|
|
143
155
|
/**
|
|
144
156
|
* @param {BridgeClientOptions} [options={}]
|
|
@@ -156,6 +168,7 @@ export class BridgeClient extends EventEmitter {
|
|
|
156
168
|
exitProcessOnNpmUpdate = false,
|
|
157
169
|
updateCompatibleNpmPackageFn = updateCompatibleNpmPackage,
|
|
158
170
|
authToken = undefined,
|
|
171
|
+
agentSession = getDefaultAgentSession(),
|
|
159
172
|
} = {}) {
|
|
160
173
|
super();
|
|
161
174
|
this.transport = socketPath ? createSocketBridgeTransport(socketPath) : transport;
|
|
@@ -171,6 +184,7 @@ export class BridgeClient extends EventEmitter {
|
|
|
171
184
|
this.exitProcessOnNpmUpdate = exitProcessOnNpmUpdate;
|
|
172
185
|
this.updateCompatibleNpmPackageFn = updateCompatibleNpmPackageFn;
|
|
173
186
|
this.authToken = authToken;
|
|
187
|
+
this.agentSession = agentSession;
|
|
174
188
|
this.socket = null;
|
|
175
189
|
this.connected = false;
|
|
176
190
|
this.protocolCompatibility = null;
|
|
@@ -488,7 +502,10 @@ export class BridgeClient extends EventEmitter {
|
|
|
488
502
|
method,
|
|
489
503
|
params,
|
|
490
504
|
tabId,
|
|
491
|
-
meta
|
|
505
|
+
meta:
|
|
506
|
+
this.agentSession && meta.agent_session === undefined
|
|
507
|
+
? { ...meta, agent_session: this.agentSession }
|
|
508
|
+
: meta,
|
|
492
509
|
});
|
|
493
510
|
|
|
494
511
|
if (method === 'page.extract_content') {
|
|
@@ -124,6 +124,8 @@ export interface BridgeClientOptions {
|
|
|
124
124
|
supportedVersions: readonly string[];
|
|
125
125
|
}) => Promise<NpmUpdateResult>;
|
|
126
126
|
authToken?: string | null;
|
|
127
|
+
/** Stable agent session id used for sticky working-tab routing. */
|
|
128
|
+
agentSession?: string | null;
|
|
127
129
|
}
|
|
128
130
|
|
|
129
131
|
export interface ShortcutCommand {
|
|
@@ -326,10 +326,17 @@ export const INPUT_ACTION_METHODS = {
|
|
|
326
326
|
*
|
|
327
327
|
* @param {unknown} elementRef
|
|
328
328
|
* @param {unknown} selector
|
|
329
|
+
* @param {unknown} [locator]
|
|
329
330
|
* @returns {import('../../protocol/src/types.js').InputTarget | null}
|
|
330
331
|
*/
|
|
331
|
-
function createInputTarget(elementRef, selector) {
|
|
332
|
+
function createInputTarget(elementRef, selector, locator) {
|
|
332
333
|
if (typeof elementRef === 'string' && elementRef) return { elementRef };
|
|
334
|
+
if (locator && typeof locator === 'object' && Object.keys(locator).length) {
|
|
335
|
+
return {
|
|
336
|
+
...(typeof selector === 'string' && selector ? { selector } : {}),
|
|
337
|
+
.../** @type {Record<string, unknown>} */ (locator),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
333
340
|
if (typeof selector === 'string' && selector) return { selector };
|
|
334
341
|
return null;
|
|
335
342
|
}
|
|
@@ -337,18 +344,20 @@ function createInputTarget(elementRef, selector) {
|
|
|
337
344
|
/**
|
|
338
345
|
* Avoid changing legacy handler payloads when new options are omitted.
|
|
339
346
|
*
|
|
340
|
-
* @param {{ executionMode?: 'dom' | 'cdp', recoverStale?: boolean }} args
|
|
341
|
-
* @returns {{ executionMode?: 'dom' | 'cdp', recoverStale?: boolean }}
|
|
347
|
+
* @param {{ executionMode?: 'dom' | 'cdp', recoverStale?: boolean, timeoutMs?: number, observe?: boolean | { settleMs?: number } }} args
|
|
348
|
+
* @returns {{ executionMode?: 'dom' | 'cdp', recoverStale?: boolean, timeoutMs?: number, observe?: boolean | { settleMs?: number } }}
|
|
342
349
|
*/
|
|
343
350
|
function createInputOptions(args) {
|
|
344
351
|
return {
|
|
345
352
|
...(args.executionMode ? { executionMode: args.executionMode } : {}),
|
|
346
353
|
...(typeof args.recoverStale === 'boolean' ? { recoverStale: args.recoverStale } : {}),
|
|
354
|
+
...(typeof args.timeoutMs === 'number' ? { timeoutMs: args.timeoutMs } : {}),
|
|
355
|
+
...(args.observe !== undefined ? { observe: args.observe } : {}),
|
|
347
356
|
};
|
|
348
357
|
}
|
|
349
358
|
|
|
350
359
|
/**
|
|
351
|
-
* @param {{ action: string, elementRef?: string, selector?: string, button?: string, clickCount?: number, holdMs?: number, text?: string, value?: string, mode?: 'auto' | 'setter' | 'keystrokes', executionMode?: 'dom' | 'cdp', recoverStale?: boolean, clear?: boolean, submit?: boolean, key?: string, code?: string, modifiers?: string[], checked?: boolean, values?: string[], labels?: string[], indexes?: number[], duration?: number, sourceElementRef?: string, sourceSelector?: string, destinationElementRef?: string, destinationSelector?: string, offsetX?: number, offsetY?: number, tabId?: number, destinationId?: string, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
|
|
360
|
+
* @param {{ action: string, elementRef?: string, selector?: string, button?: string, clickCount?: number, holdMs?: number, text?: string, value?: string, mode?: 'auto' | 'setter' | 'keystrokes', executionMode?: 'dom' | 'cdp', recoverStale?: boolean, clear?: boolean, submit?: boolean, key?: string, code?: string, modifiers?: string[], checked?: boolean, values?: string[], labels?: string[], indexes?: number[], duration?: number, sourceElementRef?: string, sourceSelector?: string, destinationElementRef?: string, destinationSelector?: string, offsetX?: number, offsetY?: number, tabId?: number, destinationId?: string, budgetPreset?: 'quick' | 'normal' | 'deep', locator?: Record<string, unknown>, timeoutMs?: number, observe?: boolean | { settleMs?: number } }} args
|
|
352
361
|
* @returns {Promise<ToolResult>}
|
|
353
362
|
*/
|
|
354
363
|
export async function handleInputTool(args) {
|
|
@@ -374,9 +383,9 @@ export async function handleInputTool(args) {
|
|
|
374
383
|
async (client) => {
|
|
375
384
|
const requestedTabId = typeof args.tabId === 'number' ? args.tabId : null;
|
|
376
385
|
const elementTarget = () => {
|
|
377
|
-
const target = createInputTarget(args.elementRef, args.selector);
|
|
386
|
+
const target = createInputTarget(args.elementRef, args.selector, args.locator);
|
|
378
387
|
if (!target) {
|
|
379
|
-
throw new BridgeError('INVALID_REQUEST', 'Provide
|
|
388
|
+
throw new BridgeError('INVALID_REQUEST', 'Provide elementRef, selector, or locator.');
|
|
380
389
|
}
|
|
381
390
|
return target;
|
|
382
391
|
};
|
|
@@ -457,7 +466,8 @@ export async function handleInputTool(args) {
|
|
|
457
466
|
return summarizeToolResponse(response, 'input.fill');
|
|
458
467
|
}
|
|
459
468
|
case 'press_key': {
|
|
460
|
-
const target =
|
|
469
|
+
const target =
|
|
470
|
+
args.elementRef || args.selector || args.locator ? elementTarget() : undefined;
|
|
461
471
|
const response = await requestBridgeWithRetry(
|
|
462
472
|
client,
|
|
463
473
|
'input.press_key',
|
|
@@ -87,6 +87,7 @@ export const DOM_ACTIONS = {
|
|
|
87
87
|
exact: a.exact,
|
|
88
88
|
selector: a.selector,
|
|
89
89
|
maxResults: a.maxResults,
|
|
90
|
+
...(a.includeHidden !== undefined ? { includeHidden: a.includeHidden } : {}),
|
|
90
91
|
}),
|
|
91
92
|
},
|
|
92
93
|
find_role: {
|
|
@@ -95,8 +96,10 @@ export const DOM_ACTIONS = {
|
|
|
95
96
|
params: (a) => ({
|
|
96
97
|
role: a.role,
|
|
97
98
|
name: a.name,
|
|
99
|
+
...(a.exact !== undefined ? { exact: a.exact } : {}),
|
|
98
100
|
selector: a.selector,
|
|
99
101
|
maxResults: a.maxResults,
|
|
102
|
+
...(a.includeHidden !== undefined ? { includeHidden: a.includeHidden } : {}),
|
|
100
103
|
}),
|
|
101
104
|
},
|
|
102
105
|
html: {
|
|
@@ -117,12 +120,14 @@ export const DOM_ACTIONS = {
|
|
|
117
120
|
maxDepth: a.maxDepth,
|
|
118
121
|
compact: a.compact,
|
|
119
122
|
interactiveOnly: a.interactiveOnly,
|
|
123
|
+
...(a.source !== undefined ? { source: a.source } : {}),
|
|
124
|
+
...(a.format !== undefined ? { format: a.format } : {}),
|
|
120
125
|
}),
|
|
121
126
|
},
|
|
122
127
|
};
|
|
123
128
|
|
|
124
129
|
/**
|
|
125
|
-
* @param {{ action: string, baselineId?: string, maxChanges?: number, selector?: string, elementRef?: string, withinRef?: string, maxNodes?: number, maxDepth?: number, compact?: boolean, interactiveOnly?: boolean, textBudget?: number, includeBbox?: boolean, attributeAllowlist?: string[], attributes?: string[], text?: string, exact?: boolean, maxResults?: number, role?: string, name?: string, state?: string, timeoutMs?: number, outer?: boolean, maxLength?: number, tabId?: number, destinationId?: string, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
|
|
130
|
+
* @param {{ action: string, baselineId?: string, maxChanges?: number, selector?: string, elementRef?: string, withinRef?: string, maxNodes?: number, maxDepth?: number, compact?: boolean, interactiveOnly?: boolean, source?: 'cdp' | 'dom', format?: 'tree' | 'outline', includeHidden?: boolean, textBudget?: number, includeBbox?: boolean, attributeAllowlist?: string[], attributes?: string[], text?: string, exact?: boolean, maxResults?: number, role?: string, name?: string, state?: string, timeoutMs?: number, outer?: boolean, maxLength?: number, tabId?: number, destinationId?: string, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
|
|
126
131
|
* @returns {Promise<ToolResult>}
|
|
127
132
|
*/
|
|
128
133
|
export async function handleDomTool(args) {
|
|
@@ -47,6 +47,8 @@ import { annotateBridgeSummary, summarizeBridgeResponse } from '../../agent-clie
|
|
|
47
47
|
|
|
48
48
|
export const REQUEST_SOURCE = 'mcp';
|
|
49
49
|
const MCP_CLIENT_ID = `mcp_${randomUUID()}`;
|
|
50
|
+
// One MCP server process serves one agent conversation, so it owns one working tab.
|
|
51
|
+
const MCP_AGENT_SESSION = process.env.BBX_SESSION?.trim() || MCP_CLIENT_ID;
|
|
50
52
|
/** @type {AsyncLocalStorage<{ era: import('../../protocol/src/types.js').McpProtocolEra, signal?: AbortSignal }>} */
|
|
51
53
|
const MCP_REQUEST_ERA = new AsyncLocalStorage();
|
|
52
54
|
|
|
@@ -346,6 +348,7 @@ export async function withMcpRequestClient(callback, options = {}) {
|
|
|
346
348
|
const client = await createBridgeClientForDestination(options.destinationId, {
|
|
347
349
|
checkProtocolOnConnect: false,
|
|
348
350
|
clientId: MCP_CLIENT_ID,
|
|
351
|
+
agentSession: MCP_AGENT_SESSION,
|
|
349
352
|
});
|
|
350
353
|
let completed = false;
|
|
351
354
|
try {
|
|
@@ -284,7 +284,7 @@ export function createBridgeMcpServer(options = {}) {
|
|
|
284
284
|
{
|
|
285
285
|
title: 'Browser DOM',
|
|
286
286
|
description:
|
|
287
|
-
'Query, describe, read, search, or wait for DOM elements. Reuse elementRef from prior results. For
|
|
287
|
+
'Query, describe, read, search, or wait for DOM elements. Reuse elementRef from prior results. For a page overview to act on, use accessibility_tree with source "dom": a compact outline (`- role "name" [ref]`) whose refs work directly as input targets, with no debugger. find_text/find_role search shadow DOM, use real accessible names (labels, aria-*), and skip hidden matches. For full-page text, use browser_page action "text".',
|
|
288
288
|
inputSchema: z.object({
|
|
289
289
|
action: z
|
|
290
290
|
.enum([
|
|
@@ -350,6 +350,16 @@ export function createBridgeMcpServer(options = {}) {
|
|
|
350
350
|
.describe(
|
|
351
351
|
'Return semantically interactive AX nodes; actionable state is reported separately'
|
|
352
352
|
),
|
|
353
|
+
source: z
|
|
354
|
+
.enum(['dom', 'cdp'])
|
|
355
|
+
.optional()
|
|
356
|
+
.describe(
|
|
357
|
+
'accessibility_tree source: dom = actionable outline with refs, no debugger (recommended); cdp = Chrome AX tree (default)'
|
|
358
|
+
),
|
|
359
|
+
format: z
|
|
360
|
+
.enum(['outline', 'tree'])
|
|
361
|
+
.optional()
|
|
362
|
+
.describe('accessibility_tree format (outline is default for source dom)'),
|
|
353
363
|
textBudget: z
|
|
354
364
|
.number()
|
|
355
365
|
.int()
|
|
@@ -381,6 +391,10 @@ export function createBridgeMcpServer(options = {}) {
|
|
|
381
391
|
.describe('Maximum search results (default: 10)'),
|
|
382
392
|
role: z.string().optional().describe('ARIA role to search for (for find_role action)'),
|
|
383
393
|
name: z.string().optional().describe('Accessible name to match with role'),
|
|
394
|
+
includeHidden: z
|
|
395
|
+
.boolean()
|
|
396
|
+
.optional()
|
|
397
|
+
.describe('Include hidden matches in find_text/find_role (default: false; counted)'),
|
|
384
398
|
state: z
|
|
385
399
|
.enum(['attached', 'detached', 'visible', 'hidden'])
|
|
386
400
|
.optional()
|
|
@@ -658,7 +672,7 @@ export function createBridgeMcpServer(options = {}) {
|
|
|
658
672
|
{
|
|
659
673
|
title: 'Browser Input',
|
|
660
674
|
description:
|
|
661
|
-
'Dispatch browser input.
|
|
675
|
+
'Dispatch browser input. Target by elementRef, selector, or a semantic locator ({role,name} / text / label / placeholder / testId) in one call. Targeted actions auto-wait for the element to become actionable (timeoutMs) and click/press_key/set_checked/select_option/submits return observed effects (URL change, dialogs, alerts, focus, DOM churn, target state), so a follow-up read is usually unnecessary. cdp_press_key and scroll_into_view use separate contracts.',
|
|
662
676
|
inputSchema: z.object({
|
|
663
677
|
action: z
|
|
664
678
|
.enum([
|
|
@@ -769,6 +783,39 @@ export function createBridgeMcpServer(options = {}) {
|
|
|
769
783
|
.describe('Drag destination selector (alternative to destinationElementRef)'),
|
|
770
784
|
offsetX: z.number().optional().describe('Drag drop offset X (default: 0)'),
|
|
771
785
|
offsetY: z.number().optional().describe('Drag drop offset Y (default: 0)'),
|
|
786
|
+
locator: z
|
|
787
|
+
.object({
|
|
788
|
+
role: z.string().max(500).optional().describe('ARIA role, e.g. button, textbox, link'),
|
|
789
|
+
name: z.string().max(500).optional().describe('Accessible name (labels, aria-*, text)'),
|
|
790
|
+
text: z.string().max(500).optional().describe('Visible text of the element'),
|
|
791
|
+
label: z.string().max(500).optional().describe('Form control label text'),
|
|
792
|
+
placeholder: z.string().max(500).optional(),
|
|
793
|
+
testId: z.string().max(500).optional().describe('data-testid / data-test / data-qa'),
|
|
794
|
+
exact: z.boolean().optional().describe('Exact instead of substring match'),
|
|
795
|
+
nth: z.number().int().min(0).max(49).optional().describe('Pick the nth visible match'),
|
|
796
|
+
})
|
|
797
|
+
.optional()
|
|
798
|
+
.describe(
|
|
799
|
+
'Find the target semantically in the same call (instead of elementRef/selector); selector narrows candidates'
|
|
800
|
+
),
|
|
801
|
+
timeoutMs: z
|
|
802
|
+
.number()
|
|
803
|
+
.int()
|
|
804
|
+
.min(0)
|
|
805
|
+
.max(15000)
|
|
806
|
+
.optional()
|
|
807
|
+
.describe(
|
|
808
|
+
'Wait for the target to appear and become actionable (default: 2500; 0 = no wait)'
|
|
809
|
+
),
|
|
810
|
+
observe: z
|
|
811
|
+
.union([
|
|
812
|
+
z.boolean(),
|
|
813
|
+
z.object({ settleMs: z.number().int().min(0).max(5000).optional() }),
|
|
814
|
+
])
|
|
815
|
+
.optional()
|
|
816
|
+
.describe(
|
|
817
|
+
'Report effects (URL, dialogs, alerts, focus, DOM churn, target state) after the action; default on for click, press_key, set_checked, select_option, and submits'
|
|
818
|
+
),
|
|
772
819
|
}),
|
|
773
820
|
},
|
|
774
821
|
handleInputTool
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
parseJsonLines,
|
|
33
33
|
RecoveryTelemetryCollector,
|
|
34
34
|
normalizeRecoveryTelemetrySummary,
|
|
35
|
+
sanitizeIncidentalText,
|
|
35
36
|
sanitizeIncidentalValue,
|
|
36
37
|
setProtocolPackageVersion,
|
|
37
38
|
validateBridgeRequest,
|
|
@@ -54,6 +55,7 @@ import {
|
|
|
54
55
|
import { normalizeDaemonLogger } from './daemon-logger.js';
|
|
55
56
|
import { DomBaselineOwnerRegistry, isValidBaselineId } from './dom-baseline-owners.js';
|
|
56
57
|
import { writeJsonLine } from './framing.js';
|
|
58
|
+
import { WindowActionRouter } from './window-actions.js';
|
|
57
59
|
|
|
58
60
|
const DAEMON_VERSION = loadDaemonVersion();
|
|
59
61
|
setProtocolPackageVersion(DAEMON_VERSION);
|
|
@@ -65,7 +67,7 @@ setProtocolPackageVersion(DAEMON_VERSION);
|
|
|
65
67
|
/** @typedef {import('./config.js').BridgeTransport} BridgeTransport */
|
|
66
68
|
/** @typedef {import('./daemon-logger.js').DaemonLoggerLike} DaemonLoggerLike */
|
|
67
69
|
/** @typedef {'agent' | 'extension'} SocketRole */
|
|
68
|
-
/** @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 */
|
|
70
|
+
/** @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, __windowAccessUpdates?: boolean, __windowActions?: boolean, __enabledWindow?: import('../../protocol/src/window-access.js').EnabledWindowInfo | null, __lastActiveAt?: number }} ClientSocket */
|
|
69
71
|
/** @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 */
|
|
70
72
|
/**
|
|
71
73
|
* @typedef {{
|
|
@@ -410,12 +412,20 @@ export function isWindowsNamedPipePath(socketPath) {
|
|
|
410
412
|
* entry?: Record<string, unknown>,
|
|
411
413
|
* request?: BridgeRequest,
|
|
412
414
|
* status?: SetupStatus,
|
|
413
|
-
* error?: { message?: string },
|
|
415
|
+
* error?: { message?: string } | string,
|
|
414
416
|
* response?: import('../../protocol/src/types.js').BridgeResponse,
|
|
415
417
|
* browserName?: string,
|
|
416
418
|
* profileLabel?: string,
|
|
417
419
|
* browserExtensionId?: string,
|
|
418
420
|
* accessEnabled?: boolean,
|
|
421
|
+
* enabledWindow?: unknown,
|
|
422
|
+
* windowAccessUpdates?: boolean,
|
|
423
|
+
* windowActions?: boolean,
|
|
424
|
+
* action?: unknown,
|
|
425
|
+
* windowId?: unknown,
|
|
426
|
+
* enabledAt?: unknown,
|
|
427
|
+
* extensionId?: unknown,
|
|
428
|
+
* ok?: unknown,
|
|
419
429
|
* authToken?: string,
|
|
420
430
|
* at?: number
|
|
421
431
|
* artifactId?: string,
|
|
@@ -466,6 +476,7 @@ export class BridgeDaemon {
|
|
|
466
476
|
this.serverAddress = null;
|
|
467
477
|
/** @type {Map<string, ClientSocket>} */
|
|
468
478
|
this.extensionSockets = new Map();
|
|
479
|
+
this.windowActions = new WindowActionRouter(this.extensionSockets);
|
|
469
480
|
/** @type {Map<string, ClientSocket>} */
|
|
470
481
|
this.agentSockets = new Map();
|
|
471
482
|
/** @type {Set<ClientSocket>} */
|
|
@@ -788,7 +799,9 @@ export class BridgeDaemon {
|
|
|
788
799
|
if (typedSocket.destroyed) return;
|
|
789
800
|
const message = /** @type {DaemonMessage} */ (raw);
|
|
790
801
|
const admitted =
|
|
791
|
-
message?.type === 'agent.request' ||
|
|
802
|
+
message?.type === 'agent.request' ||
|
|
803
|
+
message?.type === 'extension.setup_status.request' ||
|
|
804
|
+
message?.type === 'extension.window_action.request';
|
|
792
805
|
const active = this.activeHandlers.get(typedSocket) ?? 0;
|
|
793
806
|
if (
|
|
794
807
|
admitted &&
|
|
@@ -882,6 +895,7 @@ export class BridgeDaemon {
|
|
|
882
895
|
* @returns {Promise<void>}
|
|
883
896
|
*/
|
|
884
897
|
async stopInternal() {
|
|
898
|
+
this.windowActions.clear();
|
|
885
899
|
for (const pending of this.pendingRequests.values()) {
|
|
886
900
|
clearTimeout(pending.timeoutId);
|
|
887
901
|
}
|
|
@@ -978,6 +992,17 @@ export class BridgeDaemon {
|
|
|
978
992
|
return this.handleExtensionAccessUpdate(socket, message);
|
|
979
993
|
}
|
|
980
994
|
|
|
995
|
+
if (
|
|
996
|
+
message?.type === 'extension.window_action.request' ||
|
|
997
|
+
message?.type === 'extension.window_action.result'
|
|
998
|
+
) {
|
|
999
|
+
if (socket.__role !== 'extension') return this.rejectMessageForRole(socket, message);
|
|
1000
|
+
if (message.type === 'extension.window_action.request')
|
|
1001
|
+
return this.windowActions.request(socket, message);
|
|
1002
|
+
this.windowActions.result(socket, message);
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
981
1006
|
if (message?.type === 'extension.activity') {
|
|
982
1007
|
if (socket.__role !== 'extension') {
|
|
983
1008
|
return this.rejectMessageForRole(socket, message);
|
|
@@ -1380,6 +1405,7 @@ export class BridgeDaemon {
|
|
|
1380
1405
|
if (target.__extensionId && this.extensionSockets.get(target.__extensionId) === target) {
|
|
1381
1406
|
this.extensionSockets.delete(target.__extensionId);
|
|
1382
1407
|
this.invalidateConnectedExtensionsCache();
|
|
1408
|
+
this.broadcastWindowAccess();
|
|
1383
1409
|
}
|
|
1384
1410
|
target.destroy(error instanceof Error ? error : undefined);
|
|
1385
1411
|
await this.finishPendingRequestIfExhausted(request.id, pending);
|
|
@@ -1495,6 +1521,10 @@ export class BridgeDaemon {
|
|
|
1495
1521
|
*/
|
|
1496
1522
|
handleExtensionIdentity(socket, message) {
|
|
1497
1523
|
let changed = false;
|
|
1524
|
+
if (message.windowAccessUpdates === true) {
|
|
1525
|
+
socket.__windowAccessUpdates = true;
|
|
1526
|
+
}
|
|
1527
|
+
if (message.windowActions === true) socket.__windowActions = true;
|
|
1498
1528
|
const browserName = normalizeRoutingLabel(message.browserName);
|
|
1499
1529
|
if (browserName) {
|
|
1500
1530
|
changed = changed || socket.__browserName !== browserName;
|
|
@@ -1513,6 +1543,7 @@ export class BridgeDaemon {
|
|
|
1513
1543
|
if (changed) {
|
|
1514
1544
|
this.invalidateConnectedExtensionsCache();
|
|
1515
1545
|
}
|
|
1546
|
+
this.broadcastWindowAccess();
|
|
1516
1547
|
}
|
|
1517
1548
|
|
|
1518
1549
|
/**
|
|
@@ -1522,6 +1553,24 @@ export class BridgeDaemon {
|
|
|
1522
1553
|
*/
|
|
1523
1554
|
handleExtensionAccessUpdate(socket, message) {
|
|
1524
1555
|
const accessEnabled = Boolean(message.accessEnabled);
|
|
1556
|
+
const value = message.enabledWindow;
|
|
1557
|
+
const window =
|
|
1558
|
+
value && typeof value === 'object' ? /** @type {Record<string, unknown>} */ (value) : null;
|
|
1559
|
+
socket.__enabledWindow =
|
|
1560
|
+
accessEnabled &&
|
|
1561
|
+
window &&
|
|
1562
|
+
typeof window.windowId === 'number' &&
|
|
1563
|
+
Number.isInteger(window.windowId) &&
|
|
1564
|
+
window.windowId > 0 &&
|
|
1565
|
+
typeof window.title === 'string' &&
|
|
1566
|
+
typeof window.enabledAt === 'number' &&
|
|
1567
|
+
Number.isFinite(window.enabledAt)
|
|
1568
|
+
? {
|
|
1569
|
+
windowId: window.windowId,
|
|
1570
|
+
title: sanitizeIncidentalText(window.title).slice(0, 200),
|
|
1571
|
+
enabledAt: window.enabledAt,
|
|
1572
|
+
}
|
|
1573
|
+
: null;
|
|
1525
1574
|
if (!accessEnabled && socket.__extensionId) {
|
|
1526
1575
|
this.artifactStore.deleteByExtension(socket.__extensionId);
|
|
1527
1576
|
this.domBaselines.clearForSocket(socket);
|
|
@@ -1529,9 +1578,32 @@ export class BridgeDaemon {
|
|
|
1529
1578
|
if (socket.__accessEnabled !== accessEnabled) {
|
|
1530
1579
|
socket.__accessEnabled = accessEnabled;
|
|
1531
1580
|
this.invalidateConnectedExtensionsCache();
|
|
1532
|
-
return;
|
|
1533
1581
|
}
|
|
1534
1582
|
socket.__accessEnabled = accessEnabled;
|
|
1583
|
+
this.broadcastWindowAccess();
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
/** Broadcast access metadata only to locally authenticated extension connections.
|
|
1587
|
+
* @returns {void}
|
|
1588
|
+
*/
|
|
1589
|
+
broadcastWindowAccess() {
|
|
1590
|
+
/** @type {import('../../protocol/src/window-access.js').BrowserWindowAccess[]} */
|
|
1591
|
+
const enabledWindows = [...this.extensionSockets.entries()]
|
|
1592
|
+
.filter(([, socket]) => socket.__accessEnabled === true && !socket.destroyed)
|
|
1593
|
+
.map(([extensionId, socket]) => ({
|
|
1594
|
+
extensionId,
|
|
1595
|
+
browserName: socket.__browserName ?? null,
|
|
1596
|
+
profileLabel: socket.__profileLabel ?? null,
|
|
1597
|
+
window: socket.__enabledWindow ?? null,
|
|
1598
|
+
...(socket.__windowActions ? { canControl: true } : {}),
|
|
1599
|
+
}));
|
|
1600
|
+
for (const [extensionId, socket] of this.extensionSockets) {
|
|
1601
|
+
if (socket.destroyed || !socket.__windowAccessUpdates) continue;
|
|
1602
|
+
void writeJsonLine(socket, {
|
|
1603
|
+
type: 'extension.window_access',
|
|
1604
|
+
otherEnabledWindows: enabledWindows.filter((window) => window.extensionId !== extensionId),
|
|
1605
|
+
}).catch(() => socket.destroy());
|
|
1606
|
+
}
|
|
1535
1607
|
}
|
|
1536
1608
|
|
|
1537
1609
|
/**
|
|
@@ -1787,6 +1859,7 @@ export class BridgeDaemon {
|
|
|
1787
1859
|
* @returns {void}
|
|
1788
1860
|
*/
|
|
1789
1861
|
handleSocketClose(socket) {
|
|
1862
|
+
this.windowActions.disconnect(socket);
|
|
1790
1863
|
this.clientSockets.delete(socket);
|
|
1791
1864
|
if (socket.__registrationTimeoutId) {
|
|
1792
1865
|
clearTimeout(socket.__registrationTimeoutId);
|
|
@@ -1799,6 +1872,7 @@ export class BridgeDaemon {
|
|
|
1799
1872
|
if (this.extensionSockets.get(socket.__extensionId) === socket) {
|
|
1800
1873
|
this.extensionSockets.delete(socket.__extensionId);
|
|
1801
1874
|
this.invalidateConnectedExtensionsCache();
|
|
1875
|
+
this.broadcastWindowAccess();
|
|
1802
1876
|
}
|
|
1803
1877
|
}
|
|
1804
1878
|
|
|
@@ -34,14 +34,17 @@ import { createNativeMessageReader, createNativeMessageWriter, writeJsonLine } f
|
|
|
34
34
|
* type?: string,
|
|
35
35
|
* browserName?: string,
|
|
36
36
|
* profileLabel?: string,
|
|
37
|
-
* browserExtensionId?: string
|
|
37
|
+
* browserExtensionId?: string,
|
|
38
|
+
* windowAccessUpdates?: boolean,
|
|
39
|
+
* windowActions?: boolean
|
|
38
40
|
* }} HostIdentityMessage
|
|
39
41
|
*/
|
|
40
42
|
|
|
41
43
|
/**
|
|
42
44
|
* @typedef {{
|
|
43
45
|
* type?: string,
|
|
44
|
-
* accessEnabled: boolean
|
|
46
|
+
* accessEnabled: boolean,
|
|
47
|
+
* enabledWindow?: unknown
|
|
45
48
|
* }} HostAccessUpdateMessage
|
|
46
49
|
*/
|
|
47
50
|
|
|
@@ -187,6 +190,23 @@ export async function runNativeHost({
|
|
|
187
190
|
});
|
|
188
191
|
return;
|
|
189
192
|
}
|
|
193
|
+
if (message.type === 'extension.window_access') {
|
|
194
|
+
await writeNativeMessageQueued({
|
|
195
|
+
type: 'host.window_access',
|
|
196
|
+
otherEnabledWindows: message.otherEnabledWindows,
|
|
197
|
+
});
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (
|
|
201
|
+
message.type === 'extension.window_action.command' ||
|
|
202
|
+
message.type === 'extension.window_action.response'
|
|
203
|
+
) {
|
|
204
|
+
await writeNativeMessageQueued({
|
|
205
|
+
...message,
|
|
206
|
+
type: message.type.replace(/^extension\./u, 'host.'),
|
|
207
|
+
});
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
190
210
|
if (
|
|
191
211
|
message.type === 'extension.setup_status.response' ||
|
|
192
212
|
message.type === 'extension.setup_status.error'
|
|
@@ -244,6 +264,8 @@ export async function runNativeHost({
|
|
|
244
264
|
browserName: message.browserName,
|
|
245
265
|
profileLabel: message.profileLabel,
|
|
246
266
|
browserExtensionId: message.browserExtensionId,
|
|
267
|
+
...(message.windowAccessUpdates === true ? { windowAccessUpdates: true } : {}),
|
|
268
|
+
...(message.windowActions === true ? { windowActions: true } : {}),
|
|
247
269
|
});
|
|
248
270
|
return;
|
|
249
271
|
}
|
|
@@ -251,6 +273,9 @@ export async function runNativeHost({
|
|
|
251
273
|
await writeJsonLine(socket, {
|
|
252
274
|
type: 'extension.access_update',
|
|
253
275
|
accessEnabled: message.accessEnabled,
|
|
276
|
+
...(message.enabledWindow !== undefined
|
|
277
|
+
? { enabledWindow: message.enabledWindow }
|
|
278
|
+
: {}),
|
|
254
279
|
});
|
|
255
280
|
return;
|
|
256
281
|
}
|
|
@@ -261,6 +286,19 @@ export async function runNativeHost({
|
|
|
261
286
|
});
|
|
262
287
|
return;
|
|
263
288
|
}
|
|
289
|
+
if (message && typeof message === 'object') {
|
|
290
|
+
const record = /** @type {Record<string, unknown>} */ (message);
|
|
291
|
+
if (
|
|
292
|
+
record.type === 'host.window_action.request' ||
|
|
293
|
+
record.type === 'host.window_action.result'
|
|
294
|
+
) {
|
|
295
|
+
await writeJsonLine(socket, {
|
|
296
|
+
...record,
|
|
297
|
+
type: record.type.replace(/^host\./u, 'extension.'),
|
|
298
|
+
});
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
264
302
|
if (isHostArtifactMessage(message)) {
|
|
265
303
|
const record = /** @type {Record<string, unknown>} */ (message);
|
|
266
304
|
await writeJsonLine(socket, {
|