@browserbridge/bbx 1.8.0 → 1.10.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.
- package/PRIVACY.md +29 -3
- package/README.md +10 -3
- package/SECURITY.md +12 -0
- package/package.json +7 -1
- package/packages/agent-client/src/cli-args.js +70 -6
- package/packages/agent-client/src/cli-batch.js +127 -59
- package/packages/agent-client/src/cli-helpers.js +8 -0
- package/packages/agent-client/src/cli-output.js +8 -1
- package/packages/agent-client/src/cli-setup-commands.js +3 -1
- package/packages/agent-client/src/cli.js +254 -26
- package/packages/agent-client/src/client.js +156 -3
- package/packages/agent-client/src/command-registry.js +14 -4
- package/packages/agent-client/src/config.js +195 -0
- package/packages/agent-client/src/mcp-config.js +8 -8
- package/packages/agent-client/src/npm-self-update.js +305 -0
- package/packages/agent-client/src/remotes.js +4 -2
- package/packages/agent-client/src/runtime.js +99 -7
- package/packages/agent-client/src/types.ts +38 -0
- package/packages/mcp-server/src/guidance.js +37 -6
- package/packages/mcp-server/src/handlers-capture.js +29 -2
- package/packages/mcp-server/src/handlers-dom.js +54 -3
- package/packages/mcp-server/src/handlers-page.js +333 -60
- package/packages/mcp-server/src/handlers-utils.js +141 -151
- package/packages/mcp-server/src/handlers.js +17 -4
- package/packages/mcp-server/src/server.js +269 -51
- package/packages/mcp-server/src/toolset.js +48 -0
- package/packages/native-host/src/artifact-store.js +353 -0
- package/packages/native-host/src/config.js +9 -1
- package/packages/native-host/src/daemon-logger.js +3 -1
- package/packages/native-host/src/daemon.js +457 -27
- package/packages/native-host/src/dom-baseline-owners.js +210 -0
- package/packages/native-host/src/native-host.js +38 -29
- package/packages/protocol/src/budget-presets.js +182 -0
- package/packages/protocol/src/capability-values.js +2 -0
- package/packages/protocol/src/defaults.js +28 -0
- package/packages/protocol/src/errors.js +48 -5
- package/packages/protocol/src/har.js +34 -0
- package/packages/protocol/src/incidental-sanitizer.js +197 -0
- package/packages/protocol/src/index.js +4 -0
- package/packages/protocol/src/json-lines.js +3 -2
- package/packages/protocol/src/protocol.js +316 -6
- package/packages/protocol/src/recovery-telemetry.js +302 -0
- package/packages/protocol/src/registry.js +226 -6
- package/packages/protocol/src/summary.js +116 -25
- package/packages/protocol/src/types.ts +490 -4
- package/skills/browser-bridge/SKILL.md +26 -8
- package/skills/browser-bridge/references/protocol.md +125 -15
- package/skills/browser-bridge/references/token-efficiency.md +17 -7
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
export const RECOVERY_WINDOW_MS = 5 * 60 * 1000;
|
|
4
|
+
export const RECOVERY_BUCKET_MS = 5 * 1000;
|
|
5
|
+
export const RECOVERY_LOOP_WINDOW_MS = 60 * 1000;
|
|
6
|
+
export const RECOVERY_LOOP_THRESHOLD = 3;
|
|
7
|
+
export const RECOVERY_MAX_GROUPS = 64;
|
|
8
|
+
export const RECOVERY_PUBLIC_COUNT_LIMIT = 10_000;
|
|
9
|
+
|
|
10
|
+
/** @type {readonly import('./types.js').RecoveryEventKind[]} */
|
|
11
|
+
export const RECOVERY_EVENT_KINDS = Object.freeze([
|
|
12
|
+
'automatic_mcp_retry',
|
|
13
|
+
'stale_ref_recovery',
|
|
14
|
+
'debugger_reattach',
|
|
15
|
+
'content_script_reinjection',
|
|
16
|
+
'native_host_reconnect',
|
|
17
|
+
'request_outcome',
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
const RECOVERY_EVENT_KIND_SET = new Set(RECOVERY_EVENT_KINDS);
|
|
21
|
+
const BUCKET_COUNT = RECOVERY_WINDOW_MS / RECOVERY_BUCKET_MS + 1;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Fixed-bucket, process-local recovery telemetry. Group names are used only to
|
|
25
|
+
* detect repeated failures and are never included in public snapshots.
|
|
26
|
+
*/
|
|
27
|
+
export class RecoveryTelemetryCollector {
|
|
28
|
+
/** @param {{ now?: () => number }} [options] */
|
|
29
|
+
constructor({ now = Date.now } = {}) {
|
|
30
|
+
this.now = now;
|
|
31
|
+
/** @type {Array<{ start: number, events: Map<import('./types.js').RecoveryEventKind, { attempts: number, successes: number, failures: number, lastEventAt: number }> } | null>} */
|
|
32
|
+
this.buckets = Array.from({ length: BUCKET_COUNT }, () => null);
|
|
33
|
+
/** @type {Map<string, { failures: [number, number, number], next: number, count: number, touchedAt: number, kind: import('./types.js').RecoveryEventKind }>} */
|
|
34
|
+
this.groups = new Map();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {import('./types.js').RecoveryEventKind} kind
|
|
39
|
+
* @param {'success' | 'failure'} outcome
|
|
40
|
+
* @param {string} [group='default']
|
|
41
|
+
*/
|
|
42
|
+
record(kind, outcome, group = 'default') {
|
|
43
|
+
if (!RECOVERY_EVENT_KIND_SET.has(kind)) return;
|
|
44
|
+
const now = normalizeNow(this.now());
|
|
45
|
+
const start = Math.floor(now / RECOVERY_BUCKET_MS) * RECOVERY_BUCKET_MS;
|
|
46
|
+
const index = Math.floor(start / RECOVERY_BUCKET_MS) % BUCKET_COUNT;
|
|
47
|
+
let bucket = this.buckets[index];
|
|
48
|
+
if (!bucket || bucket.start !== start) {
|
|
49
|
+
bucket = { start, events: new Map() };
|
|
50
|
+
this.buckets[index] = bucket;
|
|
51
|
+
}
|
|
52
|
+
const event = bucket.events.get(kind) ?? {
|
|
53
|
+
attempts: 0,
|
|
54
|
+
successes: 0,
|
|
55
|
+
failures: 0,
|
|
56
|
+
lastEventAt: 0,
|
|
57
|
+
};
|
|
58
|
+
event.attempts += 1;
|
|
59
|
+
event.successes += outcome === 'success' ? 1 : 0;
|
|
60
|
+
event.failures += outcome === 'failure' ? 1 : 0;
|
|
61
|
+
event.lastEventAt = now;
|
|
62
|
+
bucket.events.set(kind, event);
|
|
63
|
+
|
|
64
|
+
this.pruneGroups(now);
|
|
65
|
+
if (outcome !== 'failure') return;
|
|
66
|
+
const groupKey = `${kind}\u0000${group}`;
|
|
67
|
+
let state = this.groups.get(groupKey);
|
|
68
|
+
if (!state) {
|
|
69
|
+
while (this.groups.size >= RECOVERY_MAX_GROUPS) {
|
|
70
|
+
const oldest = [...this.groups].reduce((left, right) =>
|
|
71
|
+
left[1].touchedAt <= right[1].touchedAt ? left : right
|
|
72
|
+
);
|
|
73
|
+
this.groups.delete(oldest[0]);
|
|
74
|
+
}
|
|
75
|
+
state = {
|
|
76
|
+
failures: [0, 0, 0],
|
|
77
|
+
next: 0,
|
|
78
|
+
count: 0,
|
|
79
|
+
touchedAt: now,
|
|
80
|
+
kind,
|
|
81
|
+
};
|
|
82
|
+
this.groups.set(groupKey, state);
|
|
83
|
+
}
|
|
84
|
+
state.failures[state.next] = now;
|
|
85
|
+
state.next = (state.next + 1) % RECOVERY_LOOP_THRESHOLD;
|
|
86
|
+
state.count = Math.min(RECOVERY_LOOP_THRESHOLD, state.count + 1);
|
|
87
|
+
state.touchedAt = now;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** @param {import('./types.js').RecoveryScope} scope */
|
|
91
|
+
snapshot(scope) {
|
|
92
|
+
const now = normalizeNow(this.now());
|
|
93
|
+
this.pruneGroups(now);
|
|
94
|
+
/** @type {Record<import('./types.js').RecoveryEventKind, import('./types.js').RecoveryEventSummary>} */
|
|
95
|
+
const events =
|
|
96
|
+
/** @type {Record<import('./types.js').RecoveryEventKind, import('./types.js').RecoveryEventSummary>} */ ({});
|
|
97
|
+
for (const kind of RECOVERY_EVENT_KINDS) {
|
|
98
|
+
let attempts = 0;
|
|
99
|
+
let successes = 0;
|
|
100
|
+
let failures = 0;
|
|
101
|
+
let lastEventAt = 0;
|
|
102
|
+
for (const bucket of this.buckets) {
|
|
103
|
+
if (
|
|
104
|
+
!bucket ||
|
|
105
|
+
bucket.start + RECOVERY_BUCKET_MS <= now - RECOVERY_WINDOW_MS ||
|
|
106
|
+
bucket.start > now
|
|
107
|
+
) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const event = bucket.events.get(kind);
|
|
111
|
+
if (!event) continue;
|
|
112
|
+
attempts += event.attempts;
|
|
113
|
+
successes += event.successes;
|
|
114
|
+
failures += event.failures;
|
|
115
|
+
lastEventAt = Math.max(lastEventAt, event.lastEventAt);
|
|
116
|
+
}
|
|
117
|
+
const activeLoop = [...this.groups.values()].some((group) => {
|
|
118
|
+
if (group.kind !== kind || group.count < RECOVERY_LOOP_THRESHOLD) return false;
|
|
119
|
+
return group.failures.every((at) => at >= now - RECOVERY_LOOP_WINDOW_MS && at <= now);
|
|
120
|
+
});
|
|
121
|
+
events[kind] = createPublicEventSummary(
|
|
122
|
+
attempts,
|
|
123
|
+
successes,
|
|
124
|
+
failures,
|
|
125
|
+
activeLoop,
|
|
126
|
+
lastEventAt
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
scope,
|
|
131
|
+
windowMs: RECOVERY_WINDOW_MS,
|
|
132
|
+
bucketMs: RECOVERY_BUCKET_MS,
|
|
133
|
+
loopWindowMs: RECOVERY_LOOP_WINDOW_MS,
|
|
134
|
+
loopThreshold: RECOVERY_LOOP_THRESHOLD,
|
|
135
|
+
asOf: now,
|
|
136
|
+
activeLoop: RECOVERY_EVENT_KINDS.some((kind) => events[kind].activeLoop),
|
|
137
|
+
events,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** @param {number} now */
|
|
142
|
+
pruneGroups(now) {
|
|
143
|
+
const cutoff = now - RECOVERY_LOOP_WINDOW_MS;
|
|
144
|
+
for (const [key, group] of this.groups) {
|
|
145
|
+
let recent = 0;
|
|
146
|
+
for (let index = 0; index < group.count; index += 1) {
|
|
147
|
+
const at = group.failures[index];
|
|
148
|
+
if (at >= cutoff && at <= now) recent += 1;
|
|
149
|
+
}
|
|
150
|
+
if (recent === 0) this.groups.delete(key);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Strictly reconstruct a public summary from an untrusted transport value.
|
|
157
|
+
* @param {unknown} value
|
|
158
|
+
* @param {import('./types.js').RecoveryScope} expectedScope
|
|
159
|
+
* @returns {import('./types.js').RecoveryTelemetrySummary | null}
|
|
160
|
+
*/
|
|
161
|
+
export function normalizeRecoveryTelemetrySummary(value, expectedScope) {
|
|
162
|
+
const source = asRecord(value);
|
|
163
|
+
if (!source || source.scope !== expectedScope) return null;
|
|
164
|
+
const asOf = strictTimestamp(source.asOf);
|
|
165
|
+
if (asOf === null) return null;
|
|
166
|
+
const eventsSource = asRecord(source.events);
|
|
167
|
+
if (!eventsSource) return null;
|
|
168
|
+
/** @type {Record<import('./types.js').RecoveryEventKind, import('./types.js').RecoveryEventSummary>} */
|
|
169
|
+
const events =
|
|
170
|
+
/** @type {Record<import('./types.js').RecoveryEventKind, import('./types.js').RecoveryEventSummary>} */ ({});
|
|
171
|
+
for (const kind of RECOVERY_EVENT_KINDS) {
|
|
172
|
+
const event = asRecord(eventsSource[kind]);
|
|
173
|
+
if (!event) return null;
|
|
174
|
+
const attempts = rawCount(event.attempts);
|
|
175
|
+
const successes = rawCount(event.successes);
|
|
176
|
+
const failures = rawCount(event.failures);
|
|
177
|
+
const pending = rawCount(event.pending);
|
|
178
|
+
const saturated = event.saturated;
|
|
179
|
+
if (
|
|
180
|
+
attempts === null ||
|
|
181
|
+
successes === null ||
|
|
182
|
+
failures === null ||
|
|
183
|
+
pending === null ||
|
|
184
|
+
typeof saturated !== 'boolean' ||
|
|
185
|
+
successes + failures + pending !== attempts ||
|
|
186
|
+
(saturated
|
|
187
|
+
? attempts !== RECOVERY_PUBLIC_COUNT_LIMIT
|
|
188
|
+
: attempts > RECOVERY_PUBLIC_COUNT_LIMIT)
|
|
189
|
+
) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
const lastEventAt = normalizeLastEventAt(event.lastEventAt, attempts, asOf);
|
|
193
|
+
if (lastEventAt === undefined) return null;
|
|
194
|
+
events[kind] = {
|
|
195
|
+
attempts,
|
|
196
|
+
successes,
|
|
197
|
+
failures,
|
|
198
|
+
pending,
|
|
199
|
+
saturated,
|
|
200
|
+
successRate: saturated || attempts === 0 ? null : roundRate(successes / attempts),
|
|
201
|
+
failureRate: saturated || attempts === 0 ? null : roundRate(failures / attempts),
|
|
202
|
+
activeLoop: event.activeLoop === true && failures >= RECOVERY_LOOP_THRESHOLD,
|
|
203
|
+
lastEventAt,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
scope: expectedScope,
|
|
208
|
+
windowMs: RECOVERY_WINDOW_MS,
|
|
209
|
+
bucketMs: RECOVERY_BUCKET_MS,
|
|
210
|
+
loopWindowMs: RECOVERY_LOOP_WINDOW_MS,
|
|
211
|
+
loopThreshold: RECOVERY_LOOP_THRESHOLD,
|
|
212
|
+
asOf,
|
|
213
|
+
activeLoop: RECOVERY_EVENT_KINDS.some((kind) => events[kind].activeLoop),
|
|
214
|
+
events,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** @param {unknown} value */
|
|
219
|
+
function asRecord(value) {
|
|
220
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
221
|
+
? /** @type {Record<string, unknown>} */ (value)
|
|
222
|
+
: null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** @param {unknown} value @returns {number | null} */
|
|
226
|
+
function rawCount(value) {
|
|
227
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** @param {unknown} value @returns {number | null} */
|
|
231
|
+
function strictTimestamp(value) {
|
|
232
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** @param {number} value */
|
|
236
|
+
function normalizeNow(value) {
|
|
237
|
+
return Number.isSafeInteger(value) && value >= 0 ? value : 0;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* @param {unknown} value
|
|
242
|
+
* @param {number} attempts
|
|
243
|
+
* @param {number} asOf
|
|
244
|
+
* @returns {number | null | undefined}
|
|
245
|
+
*/
|
|
246
|
+
function normalizeLastEventAt(value, attempts, asOf) {
|
|
247
|
+
if (attempts === 0) return value === null ? null : undefined;
|
|
248
|
+
const timestamp = strictTimestamp(value);
|
|
249
|
+
if (
|
|
250
|
+
timestamp === null ||
|
|
251
|
+
timestamp > asOf ||
|
|
252
|
+
timestamp < asOf - RECOVERY_WINDOW_MS - RECOVERY_BUCKET_MS
|
|
253
|
+
) {
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
return timestamp;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** @param {number} value */
|
|
260
|
+
function roundRate(value) {
|
|
261
|
+
return Math.round(value * 10_000) / 10_000;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* @param {number} attempts
|
|
266
|
+
* @param {number} successes
|
|
267
|
+
* @param {number} failures
|
|
268
|
+
* @param {boolean} activeLoop
|
|
269
|
+
* @param {number} lastEventAt
|
|
270
|
+
* @returns {import('./types.js').RecoveryEventSummary}
|
|
271
|
+
*/
|
|
272
|
+
function createPublicEventSummary(attempts, successes, failures, activeLoop, lastEventAt) {
|
|
273
|
+
if (attempts <= RECOVERY_PUBLIC_COUNT_LIMIT) {
|
|
274
|
+
return {
|
|
275
|
+
attempts,
|
|
276
|
+
successes,
|
|
277
|
+
failures,
|
|
278
|
+
pending: 0,
|
|
279
|
+
saturated: false,
|
|
280
|
+
successRate: attempts > 0 ? roundRate(successes / attempts) : null,
|
|
281
|
+
failureRate: attempts > 0 ? roundRate(failures / attempts) : null,
|
|
282
|
+
activeLoop,
|
|
283
|
+
lastEventAt: attempts > 0 ? lastEventAt : null,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
const proportionalFailures = Math.floor((failures / attempts) * RECOVERY_PUBLIC_COUNT_LIMIT);
|
|
287
|
+
const cappedFailures = Math.min(
|
|
288
|
+
RECOVERY_PUBLIC_COUNT_LIMIT,
|
|
289
|
+
Math.max(activeLoop ? RECOVERY_LOOP_THRESHOLD : 0, proportionalFailures)
|
|
290
|
+
);
|
|
291
|
+
return {
|
|
292
|
+
attempts: RECOVERY_PUBLIC_COUNT_LIMIT,
|
|
293
|
+
successes: RECOVERY_PUBLIC_COUNT_LIMIT - cappedFailures,
|
|
294
|
+
failures: cappedFailures,
|
|
295
|
+
pending: 0,
|
|
296
|
+
saturated: true,
|
|
297
|
+
successRate: null,
|
|
298
|
+
failureRate: null,
|
|
299
|
+
activeLoop,
|
|
300
|
+
lastEventAt,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
3
|
import { CAPABILITIES } from './capability-values.js';
|
|
4
|
+
import { BridgeError, ERROR_CODES } from './errors.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* @typedef {'trivial' | 'low' | 'moderate' | 'high'} BridgeMethodComplexity
|
|
@@ -40,6 +41,7 @@ const METHOD_POLICIES = Object.freeze({
|
|
|
40
41
|
capability: CAPABILITIES.NETWORK_INTERCEPT,
|
|
41
42
|
debuggerBacked: true,
|
|
42
43
|
}),
|
|
44
|
+
sensitiveRead: Object.freeze({ capability: CAPABILITIES.SENSITIVE_READ }),
|
|
43
45
|
navigationControl: Object.freeze({ capability: CAPABILITIES.NAVIGATION_CONTROL }),
|
|
44
46
|
navigationControlDebugger: Object.freeze({
|
|
45
47
|
capability: CAPABILITIES.NAVIGATION_CONTROL,
|
|
@@ -96,6 +98,8 @@ const METHOD_POLICIES = Object.freeze({
|
|
|
96
98
|
const BRIDGE_METHOD_DESCRIPTIONS = Object.freeze({
|
|
97
99
|
'access.request':
|
|
98
100
|
'Request Browser Bridge access for the focused window. Do not repeat while access is already pending.',
|
|
101
|
+
'protocol.describe':
|
|
102
|
+
'Describe bridge method parameters and metadata by method or registry group.',
|
|
99
103
|
'tabs.list': 'List tabs in the enabled window.',
|
|
100
104
|
'tabs.create': 'Create a new tab in the enabled window.',
|
|
101
105
|
'tabs.close': 'Close a tab in the enabled window.',
|
|
@@ -110,12 +114,15 @@ const BRIDGE_METHOD_DESCRIPTIONS = Object.freeze({
|
|
|
110
114
|
'Inspect or explicitly act on the current JavaScript dialog. expectedDialogId is only a stale-decision check immediately before dispatch; Chrome cannot bind the CDP command atomically to that identifier.',
|
|
111
115
|
'page.wait_for_load_state':
|
|
112
116
|
'Wait for truthful tab-complete state and/or an event-aware URL condition.',
|
|
113
|
-
'page.get_storage': 'Read local or session storage values.',
|
|
117
|
+
'page.get_storage': 'Read local or session storage key metadata without values.',
|
|
118
|
+
'sensitive.read': 'Deliberately read one exact local or session storage value.',
|
|
114
119
|
'page.get_text': 'Read bounded visible text from the page.',
|
|
115
120
|
'page.extract_content':
|
|
116
121
|
'Extract bounded semantic page content as text or Markdown without returning source HTML.',
|
|
117
122
|
'page.get_network':
|
|
118
123
|
'Read buffered fetch/XHR activity or explicitly manage bounded all-resource CDP capture.',
|
|
124
|
+
'network.export_har':
|
|
125
|
+
'Export bounded captured network evidence as a HAR 1.2 document, inline or as a daemon-owned artifact.',
|
|
119
126
|
'network.intercept.add': 'Add a request interception rule (CDP Fetch domain).',
|
|
120
127
|
'network.intercept.remove': 'Remove a request interception rule by ID.',
|
|
121
128
|
'network.intercept.list': 'List active interception rules.',
|
|
@@ -125,6 +132,10 @@ const BRIDGE_METHOD_DESCRIPTIONS = Object.freeze({
|
|
|
125
132
|
'navigation.go_back': 'Navigate backward in tab history.',
|
|
126
133
|
'navigation.go_forward': 'Navigate forward in tab history.',
|
|
127
134
|
'dom.query': 'Query a DOM subtree and return compact node summaries.',
|
|
135
|
+
'dom.baseline.create': 'Create a short-lived compact semantic DOM baseline for later comparison.',
|
|
136
|
+
'dom.baseline.compare': 'Compare the current DOM with a retained semantic baseline.',
|
|
137
|
+
'dom.baseline.describe': 'Describe a retained semantic DOM baseline and its scope.',
|
|
138
|
+
'dom.baseline.release': 'Release a retained semantic DOM baseline.',
|
|
128
139
|
'dom.describe': 'Describe one element by elementRef.',
|
|
129
140
|
'dom.get_text': 'Read bounded text for one element.',
|
|
130
141
|
'dom.get_attributes': 'Read selected attributes for one element.',
|
|
@@ -156,6 +167,8 @@ const BRIDGE_METHOD_DESCRIPTIONS = Object.freeze({
|
|
|
156
167
|
'screenshot.capture_region': 'Capture a screenshot of a viewport region.',
|
|
157
168
|
'screenshot.capture_element': 'Capture a screenshot of one element.',
|
|
158
169
|
'screenshot.capture_full_page': 'Capture a full-page screenshot beyond the viewport.',
|
|
170
|
+
'artifact.read': 'Read one bounded chunk from a daemon-owned artifact.',
|
|
171
|
+
'artifact.delete': 'Delete one daemon-owned artifact.',
|
|
159
172
|
'patch.apply_styles': 'Apply a reversible inline style patch.',
|
|
160
173
|
'patch.apply_dom': 'Apply a reversible DOM patch.',
|
|
161
174
|
'patch.list': 'List active reversible patches.',
|
|
@@ -167,7 +180,8 @@ const BRIDGE_METHOD_DESCRIPTIONS = Object.freeze({
|
|
|
167
180
|
'cdp.get_box_model': 'Read a CDP box model for a node.',
|
|
168
181
|
'cdp.get_computed_styles_for_node': 'Read CDP computed styles for a node.',
|
|
169
182
|
'cdp.dispatch_key_event': 'Dispatch a key press through Chrome DevTools Protocol input.',
|
|
170
|
-
'performance.get_metrics':
|
|
183
|
+
'performance.get_metrics':
|
|
184
|
+
'Read a raw Chrome/CDP counter point sample; names and units vary, and BBX measures no navigation window, LCP, CLS, or INP.',
|
|
171
185
|
'log.tail': 'Tail recent bridge log entries.',
|
|
172
186
|
'health.ping': 'Check daemon, extension, and access-routing health.',
|
|
173
187
|
'daemon.metrics': 'Daemon health and performance metrics.',
|
|
@@ -211,6 +225,13 @@ function createRegistryEntry(
|
|
|
211
225
|
export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
212
226
|
// system - trivial
|
|
213
227
|
'access.request': createRegistryEntry('access.request', 'system', false, ['intent'], 'trivial'),
|
|
228
|
+
'protocol.describe': createRegistryEntry(
|
|
229
|
+
'protocol.describe',
|
|
230
|
+
'system',
|
|
231
|
+
false,
|
|
232
|
+
['method', 'group'],
|
|
233
|
+
'trivial'
|
|
234
|
+
),
|
|
214
235
|
'skill.get_runtime_context': createRegistryEntry(
|
|
215
236
|
'skill.get_runtime_context',
|
|
216
237
|
'system',
|
|
@@ -304,6 +325,14 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
304
325
|
'low',
|
|
305
326
|
METHOD_POLICIES.pageRead
|
|
306
327
|
),
|
|
328
|
+
'sensitive.read': createRegistryEntry(
|
|
329
|
+
'sensitive.read',
|
|
330
|
+
'sensitive',
|
|
331
|
+
true,
|
|
332
|
+
['source', 'key'],
|
|
333
|
+
'low',
|
|
334
|
+
METHOD_POLICIES.sensitiveRead
|
|
335
|
+
),
|
|
307
336
|
'page.get_text': createRegistryEntry(
|
|
308
337
|
'page.get_text',
|
|
309
338
|
'page',
|
|
@@ -328,6 +357,14 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
328
357
|
'low',
|
|
329
358
|
METHOD_POLICIES.networkRead
|
|
330
359
|
),
|
|
360
|
+
'network.export_har': createRegistryEntry(
|
|
361
|
+
'network.export_har',
|
|
362
|
+
'page',
|
|
363
|
+
true,
|
|
364
|
+
['limit', 'urlPattern', 'delivery'],
|
|
365
|
+
'moderate',
|
|
366
|
+
METHOD_POLICIES.networkRead
|
|
367
|
+
),
|
|
331
368
|
// network intercept - moderate (holds debugger session)
|
|
332
369
|
'network.intercept.add': createRegistryEntry(
|
|
333
370
|
'network.intercept.add',
|
|
@@ -411,6 +448,38 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
411
448
|
'low',
|
|
412
449
|
METHOD_POLICIES.domRead
|
|
413
450
|
),
|
|
451
|
+
'dom.baseline.create': createRegistryEntry(
|
|
452
|
+
'dom.baseline.create',
|
|
453
|
+
'inspect',
|
|
454
|
+
true,
|
|
455
|
+
['selector', 'maxNodes', 'maxDepth', 'textBudget', 'attributeAllowlist'],
|
|
456
|
+
'low',
|
|
457
|
+
METHOD_POLICIES.domRead
|
|
458
|
+
),
|
|
459
|
+
'dom.baseline.compare': createRegistryEntry(
|
|
460
|
+
'dom.baseline.compare',
|
|
461
|
+
'inspect',
|
|
462
|
+
true,
|
|
463
|
+
['baselineId', 'maxChanges'],
|
|
464
|
+
'low',
|
|
465
|
+
METHOD_POLICIES.domRead
|
|
466
|
+
),
|
|
467
|
+
'dom.baseline.describe': createRegistryEntry(
|
|
468
|
+
'dom.baseline.describe',
|
|
469
|
+
'inspect',
|
|
470
|
+
true,
|
|
471
|
+
['baselineId'],
|
|
472
|
+
'low',
|
|
473
|
+
METHOD_POLICIES.domRead
|
|
474
|
+
),
|
|
475
|
+
'dom.baseline.release': createRegistryEntry(
|
|
476
|
+
'dom.baseline.release',
|
|
477
|
+
'inspect',
|
|
478
|
+
true,
|
|
479
|
+
['baselineId'],
|
|
480
|
+
'low',
|
|
481
|
+
METHOD_POLICIES.domRead
|
|
482
|
+
),
|
|
414
483
|
'dom.describe': createRegistryEntry(
|
|
415
484
|
'dom.describe',
|
|
416
485
|
'inspect',
|
|
@@ -612,7 +681,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
612
681
|
'screenshot.capture_region',
|
|
613
682
|
'capture',
|
|
614
683
|
true,
|
|
615
|
-
['x', 'y', 'width', 'height', 'format', 'quality'],
|
|
684
|
+
['x', 'y', 'width', 'height', 'format', 'quality', 'delivery', 'scale'],
|
|
616
685
|
'high',
|
|
617
686
|
METHOD_POLICIES.screenshotPartialDebugger
|
|
618
687
|
),
|
|
@@ -620,7 +689,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
620
689
|
'screenshot.capture_element',
|
|
621
690
|
'capture',
|
|
622
691
|
true,
|
|
623
|
-
['elementRef', 'format', 'quality'],
|
|
692
|
+
['elementRef', 'format', 'quality', 'delivery', 'scale'],
|
|
624
693
|
'high',
|
|
625
694
|
METHOD_POLICIES.screenshotPartialDebugger
|
|
626
695
|
),
|
|
@@ -628,10 +697,24 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
628
697
|
'screenshot.capture_full_page',
|
|
629
698
|
'capture',
|
|
630
699
|
true,
|
|
631
|
-
['format', 'quality'],
|
|
700
|
+
['format', 'quality', 'delivery', 'scale'],
|
|
632
701
|
'high',
|
|
633
702
|
METHOD_POLICIES.screenshotPartialDebugger
|
|
634
703
|
),
|
|
704
|
+
'artifact.read': createRegistryEntry(
|
|
705
|
+
'artifact.read',
|
|
706
|
+
'artifact',
|
|
707
|
+
false,
|
|
708
|
+
['artifactId', 'offset', 'maxBytes'],
|
|
709
|
+
'low'
|
|
710
|
+
),
|
|
711
|
+
'artifact.delete': createRegistryEntry(
|
|
712
|
+
'artifact.delete',
|
|
713
|
+
'artifact',
|
|
714
|
+
false,
|
|
715
|
+
['artifactId'],
|
|
716
|
+
'low'
|
|
717
|
+
),
|
|
635
718
|
// patch - moderate (side effects)
|
|
636
719
|
'patch.apply_styles': createRegistryEntry(
|
|
637
720
|
'patch.apply_styles',
|
|
@@ -686,7 +769,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
686
769
|
'cdp.get_dom_snapshot',
|
|
687
770
|
'cdp',
|
|
688
771
|
true,
|
|
689
|
-
[],
|
|
772
|
+
['computedStyles'],
|
|
690
773
|
'high',
|
|
691
774
|
METHOD_POLICIES.cdpDomSnapshotDebugger
|
|
692
775
|
),
|
|
@@ -733,6 +816,70 @@ export const BRIDGE_METHODS = Object.freeze(
|
|
|
733
816
|
/** @type {ReadonlySet<import('./types.js').BridgeMethod>} */
|
|
734
817
|
export const METHOD_SET = new Set(BRIDGE_METHODS);
|
|
735
818
|
|
|
819
|
+
/** Methods whose read-only forms may run concurrently without side effects. */
|
|
820
|
+
const BATCH_SAFE_METHODS = new Set([
|
|
821
|
+
'health.ping',
|
|
822
|
+
'daemon.metrics',
|
|
823
|
+
'tabs.list',
|
|
824
|
+
'skill.get_runtime_context',
|
|
825
|
+
'setup.get_status',
|
|
826
|
+
'log.tail',
|
|
827
|
+
'page.get_state',
|
|
828
|
+
'page.get_console',
|
|
829
|
+
'page.wait_for_load_state',
|
|
830
|
+
'page.get_storage',
|
|
831
|
+
'page.get_text',
|
|
832
|
+
'page.extract_content',
|
|
833
|
+
'page.get_network',
|
|
834
|
+
'dom.query',
|
|
835
|
+
'dom.baseline.compare',
|
|
836
|
+
'dom.baseline.describe',
|
|
837
|
+
'dom.describe',
|
|
838
|
+
'dom.get_text',
|
|
839
|
+
'dom.get_attributes',
|
|
840
|
+
'dom.wait_for',
|
|
841
|
+
'dom.find_by_text',
|
|
842
|
+
'dom.find_by_role',
|
|
843
|
+
'dom.get_html',
|
|
844
|
+
'dom.get_accessibility_tree',
|
|
845
|
+
'layout.get_box_model',
|
|
846
|
+
'layout.hit_test',
|
|
847
|
+
'styles.get_computed',
|
|
848
|
+
'styles.get_matched_rules',
|
|
849
|
+
'network.intercept.list',
|
|
850
|
+
'patch.list',
|
|
851
|
+
'performance.get_metrics',
|
|
852
|
+
]);
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* Classify a normalized or raw bridge call for parallel read-only execution.
|
|
856
|
+
* Parameter checks deliberately reject malformed truthy mutation flags before
|
|
857
|
+
* protocol normalization can coerce them.
|
|
858
|
+
*
|
|
859
|
+
* @param {import('./types.js').BridgeMethod} method
|
|
860
|
+
* @param {Record<string, unknown>} [params]
|
|
861
|
+
* @returns {boolean}
|
|
862
|
+
*/
|
|
863
|
+
export function isBatchSafeBridgeCall(method, params = {}) {
|
|
864
|
+
if (!BATCH_SAFE_METHODS.has(method)) return false;
|
|
865
|
+
if (
|
|
866
|
+
(method === 'page.get_console' || method === 'page.get_network') &&
|
|
867
|
+
params.clear !== undefined &&
|
|
868
|
+
params.clear !== false
|
|
869
|
+
) {
|
|
870
|
+
return false;
|
|
871
|
+
}
|
|
872
|
+
if (
|
|
873
|
+
method === 'page.get_network' &&
|
|
874
|
+
params.source === 'cdp' &&
|
|
875
|
+
params.capture !== undefined &&
|
|
876
|
+
params.capture !== 'read'
|
|
877
|
+
) {
|
|
878
|
+
return false;
|
|
879
|
+
}
|
|
880
|
+
return true;
|
|
881
|
+
}
|
|
882
|
+
|
|
736
883
|
/**
|
|
737
884
|
* @returns {Record<string, import('./types.js').BridgeMethod[]>}
|
|
738
885
|
*/
|
|
@@ -751,6 +898,79 @@ export function createBridgeMethodGroups() {
|
|
|
751
898
|
return groups;
|
|
752
899
|
}
|
|
753
900
|
|
|
901
|
+
/**
|
|
902
|
+
* @typedef {{
|
|
903
|
+
* method: import('./types.js').BridgeMethod,
|
|
904
|
+
* group: string,
|
|
905
|
+
* tab: boolean,
|
|
906
|
+
* params: readonly string[],
|
|
907
|
+
* description: string,
|
|
908
|
+
* complexity: BridgeMethodComplexity
|
|
909
|
+
* }} BridgeMethodDescription
|
|
910
|
+
*/
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* @param {import('./types.js').BridgeMethod} method
|
|
914
|
+
* @returns {BridgeMethodDescription}
|
|
915
|
+
*/
|
|
916
|
+
function createBridgeMethodDescription(method) {
|
|
917
|
+
const entry = BRIDGE_METHOD_REGISTRY[method];
|
|
918
|
+
return {
|
|
919
|
+
method,
|
|
920
|
+
group: entry.group,
|
|
921
|
+
tab: entry.tab,
|
|
922
|
+
params: entry.params,
|
|
923
|
+
description: entry.description,
|
|
924
|
+
complexity: entry.complexity,
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/**
|
|
929
|
+
* Return a compact registry index or detailed descriptions for one method or group.
|
|
930
|
+
*
|
|
931
|
+
* @param {{ method?: unknown, group?: unknown }} [options]
|
|
932
|
+
* @returns {{ groups: Array<{ group: string, count: number }> } | BridgeMethodDescription | { group: string, methods: BridgeMethodDescription[] }}
|
|
933
|
+
*/
|
|
934
|
+
export function describeBridgeMethods(options = {}) {
|
|
935
|
+
const { method, group } = options;
|
|
936
|
+
if (method !== undefined && group !== undefined) {
|
|
937
|
+
throw new BridgeError(
|
|
938
|
+
ERROR_CODES.INVALID_REQUEST,
|
|
939
|
+
'protocol.describe accepts either method or group, not both.'
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
if (method !== undefined) {
|
|
943
|
+
if (typeof method !== 'string' || !isBridgeMethod(method)) {
|
|
944
|
+
throw new BridgeError(
|
|
945
|
+
ERROR_CODES.INVALID_REQUEST,
|
|
946
|
+
`Unknown bridge method "${String(method)}".`
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
return createBridgeMethodDescription(method);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
const groups = createBridgeMethodGroups();
|
|
953
|
+
if (group !== undefined) {
|
|
954
|
+
if (typeof group !== 'string' || !Object.hasOwn(groups, group)) {
|
|
955
|
+
throw new BridgeError(
|
|
956
|
+
ERROR_CODES.INVALID_REQUEST,
|
|
957
|
+
`Unknown bridge method group "${String(group)}".`
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
return {
|
|
961
|
+
group,
|
|
962
|
+
methods: groups[group].map(createBridgeMethodDescription),
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
return {
|
|
967
|
+
groups: Object.entries(groups).map(([groupName, methods]) => ({
|
|
968
|
+
group: groupName,
|
|
969
|
+
count: methods.length,
|
|
970
|
+
})),
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
|
|
754
974
|
/**
|
|
755
975
|
* @param {string} method
|
|
756
976
|
* @returns {method is import('./types.js').BridgeMethod}
|