@browserbridge/bbx 1.8.0 → 1.9.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 +35 -0
- package/packages/agent-client/src/cli-batch.js +121 -58
- package/packages/agent-client/src/cli-output.js +8 -1
- package/packages/agent-client/src/cli.js +227 -19
- package/packages/agent-client/src/client.js +156 -3
- package/packages/agent-client/src/command-registry.js +8 -2
- package/packages/agent-client/src/config.js +195 -0
- 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 +97 -5
- package/packages/agent-client/src/types.ts +38 -0
- 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 +180 -60
- package/packages/mcp-server/src/handlers-utils.js +35 -4
- package/packages/mcp-server/src/handlers.js +10 -3
- package/packages/mcp-server/src/server.js +75 -6
- 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 +543 -27
- package/packages/native-host/src/native-host.js +25 -2
- 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/incidental-sanitizer.js +197 -0
- package/packages/protocol/src/index.js +2 -0
- package/packages/protocol/src/protocol.js +316 -6
- package/packages/protocol/src/recovery-telemetry.js +302 -0
- package/packages/protocol/src/registry.js +143 -6
- package/packages/protocol/src/summary.js +116 -25
- package/packages/protocol/src/types.ts +489 -4
- package/skills/browser-bridge/SKILL.md +22 -8
- package/skills/browser-bridge/references/protocol.md +117 -13
- 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
|
+
}
|
|
@@ -40,6 +40,7 @@ const METHOD_POLICIES = Object.freeze({
|
|
|
40
40
|
capability: CAPABILITIES.NETWORK_INTERCEPT,
|
|
41
41
|
debuggerBacked: true,
|
|
42
42
|
}),
|
|
43
|
+
sensitiveRead: Object.freeze({ capability: CAPABILITIES.SENSITIVE_READ }),
|
|
43
44
|
navigationControl: Object.freeze({ capability: CAPABILITIES.NAVIGATION_CONTROL }),
|
|
44
45
|
navigationControlDebugger: Object.freeze({
|
|
45
46
|
capability: CAPABILITIES.NAVIGATION_CONTROL,
|
|
@@ -110,12 +111,15 @@ const BRIDGE_METHOD_DESCRIPTIONS = Object.freeze({
|
|
|
110
111
|
'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
112
|
'page.wait_for_load_state':
|
|
112
113
|
'Wait for truthful tab-complete state and/or an event-aware URL condition.',
|
|
113
|
-
'page.get_storage': 'Read local or session storage values.',
|
|
114
|
+
'page.get_storage': 'Read local or session storage key metadata without values.',
|
|
115
|
+
'sensitive.read': 'Deliberately read one exact local or session storage value.',
|
|
114
116
|
'page.get_text': 'Read bounded visible text from the page.',
|
|
115
117
|
'page.extract_content':
|
|
116
118
|
'Extract bounded semantic page content as text or Markdown without returning source HTML.',
|
|
117
119
|
'page.get_network':
|
|
118
120
|
'Read buffered fetch/XHR activity or explicitly manage bounded all-resource CDP capture.',
|
|
121
|
+
'network.export_har':
|
|
122
|
+
'Export bounded captured network evidence as a HAR 1.2 document, inline or as a daemon-owned artifact.',
|
|
119
123
|
'network.intercept.add': 'Add a request interception rule (CDP Fetch domain).',
|
|
120
124
|
'network.intercept.remove': 'Remove a request interception rule by ID.',
|
|
121
125
|
'network.intercept.list': 'List active interception rules.',
|
|
@@ -125,6 +129,10 @@ const BRIDGE_METHOD_DESCRIPTIONS = Object.freeze({
|
|
|
125
129
|
'navigation.go_back': 'Navigate backward in tab history.',
|
|
126
130
|
'navigation.go_forward': 'Navigate forward in tab history.',
|
|
127
131
|
'dom.query': 'Query a DOM subtree and return compact node summaries.',
|
|
132
|
+
'dom.baseline.create': 'Create a short-lived compact semantic DOM baseline for later comparison.',
|
|
133
|
+
'dom.baseline.compare': 'Compare the current DOM with a retained semantic baseline.',
|
|
134
|
+
'dom.baseline.describe': 'Describe a retained semantic DOM baseline and its scope.',
|
|
135
|
+
'dom.baseline.release': 'Release a retained semantic DOM baseline.',
|
|
128
136
|
'dom.describe': 'Describe one element by elementRef.',
|
|
129
137
|
'dom.get_text': 'Read bounded text for one element.',
|
|
130
138
|
'dom.get_attributes': 'Read selected attributes for one element.',
|
|
@@ -156,6 +164,8 @@ const BRIDGE_METHOD_DESCRIPTIONS = Object.freeze({
|
|
|
156
164
|
'screenshot.capture_region': 'Capture a screenshot of a viewport region.',
|
|
157
165
|
'screenshot.capture_element': 'Capture a screenshot of one element.',
|
|
158
166
|
'screenshot.capture_full_page': 'Capture a full-page screenshot beyond the viewport.',
|
|
167
|
+
'artifact.read': 'Read one bounded chunk from a daemon-owned artifact.',
|
|
168
|
+
'artifact.delete': 'Delete one daemon-owned artifact.',
|
|
159
169
|
'patch.apply_styles': 'Apply a reversible inline style patch.',
|
|
160
170
|
'patch.apply_dom': 'Apply a reversible DOM patch.',
|
|
161
171
|
'patch.list': 'List active reversible patches.',
|
|
@@ -167,7 +177,8 @@ const BRIDGE_METHOD_DESCRIPTIONS = Object.freeze({
|
|
|
167
177
|
'cdp.get_box_model': 'Read a CDP box model for a node.',
|
|
168
178
|
'cdp.get_computed_styles_for_node': 'Read CDP computed styles for a node.',
|
|
169
179
|
'cdp.dispatch_key_event': 'Dispatch a key press through Chrome DevTools Protocol input.',
|
|
170
|
-
'performance.get_metrics':
|
|
180
|
+
'performance.get_metrics':
|
|
181
|
+
'Read a raw Chrome/CDP counter point sample; names and units vary, and BBX measures no navigation window, LCP, CLS, or INP.',
|
|
171
182
|
'log.tail': 'Tail recent bridge log entries.',
|
|
172
183
|
'health.ping': 'Check daemon, extension, and access-routing health.',
|
|
173
184
|
'daemon.metrics': 'Daemon health and performance metrics.',
|
|
@@ -304,6 +315,14 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
304
315
|
'low',
|
|
305
316
|
METHOD_POLICIES.pageRead
|
|
306
317
|
),
|
|
318
|
+
'sensitive.read': createRegistryEntry(
|
|
319
|
+
'sensitive.read',
|
|
320
|
+
'sensitive',
|
|
321
|
+
true,
|
|
322
|
+
['source', 'key'],
|
|
323
|
+
'low',
|
|
324
|
+
METHOD_POLICIES.sensitiveRead
|
|
325
|
+
),
|
|
307
326
|
'page.get_text': createRegistryEntry(
|
|
308
327
|
'page.get_text',
|
|
309
328
|
'page',
|
|
@@ -328,6 +347,14 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
328
347
|
'low',
|
|
329
348
|
METHOD_POLICIES.networkRead
|
|
330
349
|
),
|
|
350
|
+
'network.export_har': createRegistryEntry(
|
|
351
|
+
'network.export_har',
|
|
352
|
+
'page',
|
|
353
|
+
true,
|
|
354
|
+
['limit', 'urlPattern', 'delivery'],
|
|
355
|
+
'moderate',
|
|
356
|
+
METHOD_POLICIES.networkRead
|
|
357
|
+
),
|
|
331
358
|
// network intercept - moderate (holds debugger session)
|
|
332
359
|
'network.intercept.add': createRegistryEntry(
|
|
333
360
|
'network.intercept.add',
|
|
@@ -411,6 +438,38 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
411
438
|
'low',
|
|
412
439
|
METHOD_POLICIES.domRead
|
|
413
440
|
),
|
|
441
|
+
'dom.baseline.create': createRegistryEntry(
|
|
442
|
+
'dom.baseline.create',
|
|
443
|
+
'inspect',
|
|
444
|
+
true,
|
|
445
|
+
['selector', 'maxNodes', 'maxDepth', 'textBudget', 'attributeAllowlist'],
|
|
446
|
+
'low',
|
|
447
|
+
METHOD_POLICIES.domRead
|
|
448
|
+
),
|
|
449
|
+
'dom.baseline.compare': createRegistryEntry(
|
|
450
|
+
'dom.baseline.compare',
|
|
451
|
+
'inspect',
|
|
452
|
+
true,
|
|
453
|
+
['baselineId', 'maxChanges'],
|
|
454
|
+
'low',
|
|
455
|
+
METHOD_POLICIES.domRead
|
|
456
|
+
),
|
|
457
|
+
'dom.baseline.describe': createRegistryEntry(
|
|
458
|
+
'dom.baseline.describe',
|
|
459
|
+
'inspect',
|
|
460
|
+
true,
|
|
461
|
+
['baselineId'],
|
|
462
|
+
'low',
|
|
463
|
+
METHOD_POLICIES.domRead
|
|
464
|
+
),
|
|
465
|
+
'dom.baseline.release': createRegistryEntry(
|
|
466
|
+
'dom.baseline.release',
|
|
467
|
+
'inspect',
|
|
468
|
+
true,
|
|
469
|
+
['baselineId'],
|
|
470
|
+
'low',
|
|
471
|
+
METHOD_POLICIES.domRead
|
|
472
|
+
),
|
|
414
473
|
'dom.describe': createRegistryEntry(
|
|
415
474
|
'dom.describe',
|
|
416
475
|
'inspect',
|
|
@@ -612,7 +671,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
612
671
|
'screenshot.capture_region',
|
|
613
672
|
'capture',
|
|
614
673
|
true,
|
|
615
|
-
['x', 'y', 'width', 'height', 'format', 'quality'],
|
|
674
|
+
['x', 'y', 'width', 'height', 'format', 'quality', 'delivery', 'scale'],
|
|
616
675
|
'high',
|
|
617
676
|
METHOD_POLICIES.screenshotPartialDebugger
|
|
618
677
|
),
|
|
@@ -620,7 +679,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
620
679
|
'screenshot.capture_element',
|
|
621
680
|
'capture',
|
|
622
681
|
true,
|
|
623
|
-
['elementRef', 'format', 'quality'],
|
|
682
|
+
['elementRef', 'format', 'quality', 'delivery', 'scale'],
|
|
624
683
|
'high',
|
|
625
684
|
METHOD_POLICIES.screenshotPartialDebugger
|
|
626
685
|
),
|
|
@@ -628,10 +687,24 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
628
687
|
'screenshot.capture_full_page',
|
|
629
688
|
'capture',
|
|
630
689
|
true,
|
|
631
|
-
['format', 'quality'],
|
|
690
|
+
['format', 'quality', 'delivery', 'scale'],
|
|
632
691
|
'high',
|
|
633
692
|
METHOD_POLICIES.screenshotPartialDebugger
|
|
634
693
|
),
|
|
694
|
+
'artifact.read': createRegistryEntry(
|
|
695
|
+
'artifact.read',
|
|
696
|
+
'artifact',
|
|
697
|
+
false,
|
|
698
|
+
['artifactId', 'offset', 'maxBytes'],
|
|
699
|
+
'low'
|
|
700
|
+
),
|
|
701
|
+
'artifact.delete': createRegistryEntry(
|
|
702
|
+
'artifact.delete',
|
|
703
|
+
'artifact',
|
|
704
|
+
false,
|
|
705
|
+
['artifactId'],
|
|
706
|
+
'low'
|
|
707
|
+
),
|
|
635
708
|
// patch - moderate (side effects)
|
|
636
709
|
'patch.apply_styles': createRegistryEntry(
|
|
637
710
|
'patch.apply_styles',
|
|
@@ -686,7 +759,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
686
759
|
'cdp.get_dom_snapshot',
|
|
687
760
|
'cdp',
|
|
688
761
|
true,
|
|
689
|
-
[],
|
|
762
|
+
['computedStyles'],
|
|
690
763
|
'high',
|
|
691
764
|
METHOD_POLICIES.cdpDomSnapshotDebugger
|
|
692
765
|
),
|
|
@@ -733,6 +806,70 @@ export const BRIDGE_METHODS = Object.freeze(
|
|
|
733
806
|
/** @type {ReadonlySet<import('./types.js').BridgeMethod>} */
|
|
734
807
|
export const METHOD_SET = new Set(BRIDGE_METHODS);
|
|
735
808
|
|
|
809
|
+
/** Methods whose read-only forms may run concurrently without side effects. */
|
|
810
|
+
const BATCH_SAFE_METHODS = new Set([
|
|
811
|
+
'health.ping',
|
|
812
|
+
'daemon.metrics',
|
|
813
|
+
'tabs.list',
|
|
814
|
+
'skill.get_runtime_context',
|
|
815
|
+
'setup.get_status',
|
|
816
|
+
'log.tail',
|
|
817
|
+
'page.get_state',
|
|
818
|
+
'page.get_console',
|
|
819
|
+
'page.wait_for_load_state',
|
|
820
|
+
'page.get_storage',
|
|
821
|
+
'page.get_text',
|
|
822
|
+
'page.extract_content',
|
|
823
|
+
'page.get_network',
|
|
824
|
+
'dom.query',
|
|
825
|
+
'dom.baseline.compare',
|
|
826
|
+
'dom.baseline.describe',
|
|
827
|
+
'dom.describe',
|
|
828
|
+
'dom.get_text',
|
|
829
|
+
'dom.get_attributes',
|
|
830
|
+
'dom.wait_for',
|
|
831
|
+
'dom.find_by_text',
|
|
832
|
+
'dom.find_by_role',
|
|
833
|
+
'dom.get_html',
|
|
834
|
+
'dom.get_accessibility_tree',
|
|
835
|
+
'layout.get_box_model',
|
|
836
|
+
'layout.hit_test',
|
|
837
|
+
'styles.get_computed',
|
|
838
|
+
'styles.get_matched_rules',
|
|
839
|
+
'network.intercept.list',
|
|
840
|
+
'patch.list',
|
|
841
|
+
'performance.get_metrics',
|
|
842
|
+
]);
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* Classify a normalized or raw bridge call for parallel read-only execution.
|
|
846
|
+
* Parameter checks deliberately reject malformed truthy mutation flags before
|
|
847
|
+
* protocol normalization can coerce them.
|
|
848
|
+
*
|
|
849
|
+
* @param {import('./types.js').BridgeMethod} method
|
|
850
|
+
* @param {Record<string, unknown>} [params]
|
|
851
|
+
* @returns {boolean}
|
|
852
|
+
*/
|
|
853
|
+
export function isBatchSafeBridgeCall(method, params = {}) {
|
|
854
|
+
if (!BATCH_SAFE_METHODS.has(method)) return false;
|
|
855
|
+
if (
|
|
856
|
+
(method === 'page.get_console' || method === 'page.get_network') &&
|
|
857
|
+
params.clear !== undefined &&
|
|
858
|
+
params.clear !== false
|
|
859
|
+
) {
|
|
860
|
+
return false;
|
|
861
|
+
}
|
|
862
|
+
if (
|
|
863
|
+
method === 'page.get_network' &&
|
|
864
|
+
params.source === 'cdp' &&
|
|
865
|
+
params.capture !== undefined &&
|
|
866
|
+
params.capture !== 'read'
|
|
867
|
+
) {
|
|
868
|
+
return false;
|
|
869
|
+
}
|
|
870
|
+
return true;
|
|
871
|
+
}
|
|
872
|
+
|
|
736
873
|
/**
|
|
737
874
|
* @returns {Record<string, import('./types.js').BridgeMethod[]>}
|
|
738
875
|
*/
|