@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
|
@@ -2,9 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
DEFAULT_CONSOLE_LIMIT,
|
|
5
|
+
DEFAULT_HAR_LIMIT,
|
|
5
6
|
DEFAULT_NETWORK_LIMIT,
|
|
7
|
+
describeBridgeMethods,
|
|
6
8
|
ERROR_CODES,
|
|
7
9
|
getErrorRecovery,
|
|
10
|
+
isBatchSafeBridgeCall,
|
|
11
|
+
isHarDocument,
|
|
12
|
+
MAX_BATCH_CALLS,
|
|
13
|
+
MAX_BATCH_CONCURRENCY,
|
|
8
14
|
METHOD_SET,
|
|
9
15
|
} from '../../protocol/src/index.js';
|
|
10
16
|
import { createBridgeClientForDestination } from '../../agent-client/src/remotes.js';
|
|
@@ -33,63 +39,6 @@ import { createScreenshotResult } from './handlers-capture.js';
|
|
|
33
39
|
/** @typedef {import('../../agent-client/src/client.js').BridgeClient} BridgeClient */
|
|
34
40
|
/** @typedef {import('./handlers-utils.js').ToolResult} ToolResult */
|
|
35
41
|
|
|
36
|
-
export const MAX_BATCH_CALLS = 20;
|
|
37
|
-
export const MAX_BATCH_CONCURRENCY = 5;
|
|
38
|
-
|
|
39
|
-
/** @type {ReadonlySet<BridgeMethod>} */
|
|
40
|
-
const BATCH_SAFE_METHODS = new Set([
|
|
41
|
-
'health.ping',
|
|
42
|
-
'daemon.metrics',
|
|
43
|
-
'tabs.list',
|
|
44
|
-
'skill.get_runtime_context',
|
|
45
|
-
'setup.get_status',
|
|
46
|
-
'log.tail',
|
|
47
|
-
'page.get_state',
|
|
48
|
-
'page.get_console',
|
|
49
|
-
'page.wait_for_load_state',
|
|
50
|
-
'page.get_storage',
|
|
51
|
-
'page.get_text',
|
|
52
|
-
'page.extract_content',
|
|
53
|
-
'page.get_network',
|
|
54
|
-
'dom.query',
|
|
55
|
-
'dom.describe',
|
|
56
|
-
'dom.get_text',
|
|
57
|
-
'dom.get_attributes',
|
|
58
|
-
'dom.wait_for',
|
|
59
|
-
'dom.find_by_text',
|
|
60
|
-
'dom.find_by_role',
|
|
61
|
-
'dom.get_html',
|
|
62
|
-
'dom.get_accessibility_tree',
|
|
63
|
-
'layout.get_box_model',
|
|
64
|
-
'layout.hit_test',
|
|
65
|
-
'styles.get_computed',
|
|
66
|
-
'styles.get_matched_rules',
|
|
67
|
-
'network.intercept.list',
|
|
68
|
-
'patch.list',
|
|
69
|
-
'performance.get_metrics',
|
|
70
|
-
]);
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* @param {BridgeMethod} method
|
|
74
|
-
* @param {Record<string, unknown>} params
|
|
75
|
-
* @returns {boolean}
|
|
76
|
-
*/
|
|
77
|
-
export function isBatchSafeBridgeCall(method, params) {
|
|
78
|
-
if (!BATCH_SAFE_METHODS.has(method)) return false;
|
|
79
|
-
if ((method === 'page.get_console' || method === 'page.get_network') && params.clear === true) {
|
|
80
|
-
return false;
|
|
81
|
-
}
|
|
82
|
-
if (
|
|
83
|
-
method === 'page.get_network' &&
|
|
84
|
-
params.source === 'cdp' &&
|
|
85
|
-
params.capture !== undefined &&
|
|
86
|
-
params.capture !== 'read'
|
|
87
|
-
) {
|
|
88
|
-
return false;
|
|
89
|
-
}
|
|
90
|
-
return true;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
42
|
/** @type {Record<string, { method: BridgeMethod, params: (a: Record<string, unknown>) => Record<string, unknown> }>} */
|
|
94
43
|
export const PAGE_ACTIONS = {
|
|
95
44
|
state: { method: 'page.get_state', params: () => ({}) },
|
|
@@ -152,11 +101,19 @@ export const PAGE_ACTIONS = {
|
|
|
152
101
|
capture: a.capture,
|
|
153
102
|
}),
|
|
154
103
|
},
|
|
104
|
+
har: {
|
|
105
|
+
method: 'network.export_har',
|
|
106
|
+
params: (a) => ({
|
|
107
|
+
limit: a.limit,
|
|
108
|
+
urlPattern: a.urlPattern,
|
|
109
|
+
delivery: a.delivery,
|
|
110
|
+
}),
|
|
111
|
+
},
|
|
155
112
|
performance: { method: 'performance.get_metrics', params: () => ({}) },
|
|
156
113
|
};
|
|
157
114
|
|
|
158
115
|
/**
|
|
159
|
-
* @param {{ action: string, expression?: string, awaitPromise?: boolean, timeoutMs?: number, returnByValue?: boolean, level?: string, clear?: boolean, limit?: number, type?: string, keys?: string[], textBudget?: number, format?: 'text' | 'markdown', selector?: string, includeMetadata?: boolean, consistency?: 'best_effort' | 'settled', settleTimeoutMs?: number, urlPattern?: string, source?: 'fetch-xhr' | 'cdp', capture?: 'read' | 'start' | 'clear' | 'stop', dialogAction?: string, promptText?: string, expectedDialogId?: string, waitForLoad?: boolean, url?: string, urlMatch?: string, tabId?: number, destinationId?: string, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
|
|
116
|
+
* @param {{ action: string, expression?: string, awaitPromise?: boolean, timeoutMs?: number, returnByValue?: boolean, level?: string, clear?: boolean, limit?: number, type?: string, keys?: string[], textBudget?: number, format?: 'text' | 'markdown', selector?: string, includeMetadata?: boolean, consistency?: 'best_effort' | 'settled', settleTimeoutMs?: number, urlPattern?: string, source?: 'fetch-xhr' | 'cdp', capture?: 'read' | 'start' | 'clear' | 'stop', delivery?: 'auto' | 'inline' | 'artifact', dialogAction?: string, promptText?: string, expectedDialogId?: string, waitForLoad?: boolean, url?: string, urlMatch?: string, tabId?: number, destinationId?: string, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
|
|
160
117
|
* @returns {Promise<ToolResult>}
|
|
161
118
|
*/
|
|
162
119
|
export async function handlePageTool(args) {
|
|
@@ -175,6 +132,12 @@ export async function handlePageTool(args) {
|
|
|
175
132
|
normal: DEFAULT_NETWORK_LIMIT,
|
|
176
133
|
deep: 100,
|
|
177
134
|
});
|
|
135
|
+
} else if (args.action === 'har') {
|
|
136
|
+
normalizedArgs = applyLimitBudgetPreset(args, {
|
|
137
|
+
quick: 20,
|
|
138
|
+
normal: DEFAULT_HAR_LIMIT,
|
|
139
|
+
deep: 100,
|
|
140
|
+
});
|
|
178
141
|
}
|
|
179
142
|
const entry = PAGE_ACTIONS[normalizedArgs.action];
|
|
180
143
|
if (!entry) return summarizeToolError(`Unsupported page action "${args.action}".`);
|
|
@@ -184,13 +147,204 @@ export async function handlePageTool(args) {
|
|
|
184
147
|
) {
|
|
185
148
|
return summarizeToolError('expression is required for page evaluate.');
|
|
186
149
|
}
|
|
187
|
-
|
|
150
|
+
const params = entry.params(normalizedArgs);
|
|
151
|
+
if (entry.method === 'network.export_har') {
|
|
152
|
+
return withToolClient(
|
|
153
|
+
async (client) => {
|
|
154
|
+
const response = await requestBridgeWithRetry(client, entry.method, params, {
|
|
155
|
+
tabId: typeof normalizedArgs.tabId === 'number' ? normalizedArgs.tabId : null,
|
|
156
|
+
source: REQUEST_SOURCE,
|
|
157
|
+
tokenBudget: getToolTokenBudget(normalizedArgs),
|
|
158
|
+
});
|
|
159
|
+
return createHarExportResult(response);
|
|
160
|
+
},
|
|
161
|
+
{ destinationId: normalizedArgs.destinationId ?? null }
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
return callBridgeTool(entry.method, params, {
|
|
188
165
|
tabId: typeof normalizedArgs.tabId === 'number' ? normalizedArgs.tabId : null,
|
|
189
166
|
tokenBudget: getToolTokenBudget(normalizedArgs),
|
|
190
167
|
destinationId: normalizedArgs.destinationId ?? null,
|
|
191
168
|
});
|
|
192
169
|
}
|
|
193
170
|
|
|
171
|
+
/**
|
|
172
|
+
* Read one chunk of a daemon-owned artifact or delete it. Reads are paged by
|
|
173
|
+
* the caller: pass the returned nextOffset until it comes back null, then
|
|
174
|
+
* verify the reassembled bytes against sha256.
|
|
175
|
+
*
|
|
176
|
+
* @param {{ action: string, artifactId?: string, offset?: number, limit?: number, destinationId?: string, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
|
|
177
|
+
* @returns {Promise<ToolResult>}
|
|
178
|
+
*/
|
|
179
|
+
export async function handleArtifactTool(args) {
|
|
180
|
+
if (args.action !== 'read' && args.action !== 'delete') {
|
|
181
|
+
return summarizeToolError(`Unsupported artifact action "${args.action}".`);
|
|
182
|
+
}
|
|
183
|
+
if (typeof args.artifactId !== 'string' || !args.artifactId.trim()) {
|
|
184
|
+
return summarizeToolError('artifactId is required.');
|
|
185
|
+
}
|
|
186
|
+
if (args.offset !== undefined && (!Number.isInteger(args.offset) || args.offset < 0)) {
|
|
187
|
+
return summarizeToolError('offset must be a non-negative integer.');
|
|
188
|
+
}
|
|
189
|
+
if (args.limit !== undefined && (!Number.isInteger(args.limit) || args.limit < 1)) {
|
|
190
|
+
return summarizeToolError('limit must be a positive integer.');
|
|
191
|
+
}
|
|
192
|
+
const method = /** @type {BridgeMethod} */ (
|
|
193
|
+
args.action === 'read' ? 'artifact.read' : 'artifact.delete'
|
|
194
|
+
);
|
|
195
|
+
const params =
|
|
196
|
+
args.action === 'read'
|
|
197
|
+
? {
|
|
198
|
+
artifactId: args.artifactId,
|
|
199
|
+
offset: args.offset ?? 0,
|
|
200
|
+
...(typeof args.limit === 'number' ? { maxBytes: args.limit } : {}),
|
|
201
|
+
}
|
|
202
|
+
: { artifactId: args.artifactId };
|
|
203
|
+
return withToolClient(
|
|
204
|
+
async (client) => {
|
|
205
|
+
const response = await requestBridgeWithRetry(client, method, params, {
|
|
206
|
+
tabId: null,
|
|
207
|
+
source: REQUEST_SOURCE,
|
|
208
|
+
tokenBudget: getToolTokenBudget(args),
|
|
209
|
+
});
|
|
210
|
+
return method === 'artifact.read'
|
|
211
|
+
? createArtifactReadResult(response)
|
|
212
|
+
: summarizeToolResponse(response, method, params);
|
|
213
|
+
},
|
|
214
|
+
{ destinationId: args.destinationId ?? null }
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Manage CDP Fetch request interception rules for a tab.
|
|
220
|
+
*
|
|
221
|
+
* @param {{ action: string, urlPattern?: string, ruleAction?: 'block' | 'fulfill' | 'continue', statusCode?: number, body?: string, headers?: Record<string, string>, ruleId?: string, tabId?: number, destinationId?: string, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
|
|
222
|
+
* @returns {Promise<ToolResult>}
|
|
223
|
+
*/
|
|
224
|
+
export async function handleInterceptTool(args) {
|
|
225
|
+
const requestedTabId = typeof args.tabId === 'number' ? args.tabId : null;
|
|
226
|
+
return withToolClient(
|
|
227
|
+
async (client) => {
|
|
228
|
+
/** @type {BridgeMethod} */
|
|
229
|
+
let method;
|
|
230
|
+
/** @type {Record<string, unknown>} */
|
|
231
|
+
let params;
|
|
232
|
+
switch (args.action) {
|
|
233
|
+
case 'add': {
|
|
234
|
+
if (typeof args.urlPattern !== 'string' || !args.urlPattern.trim()) {
|
|
235
|
+
return summarizeToolError('urlPattern is required for intercept add.');
|
|
236
|
+
}
|
|
237
|
+
method = 'network.intercept.add';
|
|
238
|
+
params = {
|
|
239
|
+
urlPattern: args.urlPattern,
|
|
240
|
+
...(args.ruleAction ? { action: args.ruleAction } : {}),
|
|
241
|
+
...(typeof args.statusCode === 'number' ? { statusCode: args.statusCode } : {}),
|
|
242
|
+
...(typeof args.body === 'string' ? { body: args.body } : {}),
|
|
243
|
+
...(args.headers ? { headers: args.headers } : {}),
|
|
244
|
+
};
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
case 'remove': {
|
|
248
|
+
if (typeof args.ruleId !== 'string' || !args.ruleId.trim()) {
|
|
249
|
+
return summarizeToolError('ruleId is required for intercept remove.');
|
|
250
|
+
}
|
|
251
|
+
method = 'network.intercept.remove';
|
|
252
|
+
params = { ruleId: args.ruleId };
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
case 'list':
|
|
256
|
+
method = 'network.intercept.list';
|
|
257
|
+
params = {};
|
|
258
|
+
break;
|
|
259
|
+
case 'clear':
|
|
260
|
+
method = 'network.intercept.clear';
|
|
261
|
+
params = {};
|
|
262
|
+
break;
|
|
263
|
+
default:
|
|
264
|
+
return summarizeToolError(`Unsupported intercept action "${args.action}".`);
|
|
265
|
+
}
|
|
266
|
+
const response = await requestBridgeWithRetry(client, method, params, {
|
|
267
|
+
tabId: requestedTabId,
|
|
268
|
+
source: REQUEST_SOURCE,
|
|
269
|
+
tokenBudget: getToolTokenBudget(args),
|
|
270
|
+
});
|
|
271
|
+
return summarizeToolResponse(response, method, params);
|
|
272
|
+
},
|
|
273
|
+
{ destinationId: args.destinationId ?? null }
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* @param {import('../../protocol/src/types.js').BridgeResponse} response
|
|
279
|
+
* @returns {ToolResult}
|
|
280
|
+
*/
|
|
281
|
+
function createArtifactReadResult(response) {
|
|
282
|
+
if (!response.ok) return summarizeToolResponse(response, 'artifact.read', {});
|
|
283
|
+
const result =
|
|
284
|
+
response.result && typeof response.result === 'object' && !Array.isArray(response.result)
|
|
285
|
+
? /** @type {Record<string, unknown>} */ (response.result)
|
|
286
|
+
: {};
|
|
287
|
+
if (
|
|
288
|
+
typeof result.artifactId !== 'string' ||
|
|
289
|
+
typeof result.data !== 'string' ||
|
|
290
|
+
typeof result.offset !== 'number' ||
|
|
291
|
+
typeof result.byteLength !== 'number' ||
|
|
292
|
+
typeof result.totalBytes !== 'number' ||
|
|
293
|
+
typeof result.sha256 !== 'string' ||
|
|
294
|
+
(result.nextOffset !== null && typeof result.nextOffset !== 'number')
|
|
295
|
+
) {
|
|
296
|
+
return summarizeToolError('artifact.read returned invalid chunk metadata.');
|
|
297
|
+
}
|
|
298
|
+
const nextOffset = /** @type {number | null} */ (result.nextOffset);
|
|
299
|
+
return createToolResult(
|
|
300
|
+
`Read ${result.byteLength} bytes from artifact ${result.artifactId} at offset ${result.offset} (${result.totalBytes} bytes total). ${
|
|
301
|
+
nextOffset === null
|
|
302
|
+
? 'Artifact fully read; verify the reassembled bytes against sha256.'
|
|
303
|
+
: `Continue reading with offset ${nextOffset}.`
|
|
304
|
+
}`,
|
|
305
|
+
{
|
|
306
|
+
ok: true,
|
|
307
|
+
method: 'artifact.read',
|
|
308
|
+
artifactId: result.artifactId,
|
|
309
|
+
data: result.data,
|
|
310
|
+
offset: result.offset,
|
|
311
|
+
byteLength: result.byteLength,
|
|
312
|
+
nextOffset,
|
|
313
|
+
totalBytes: result.totalBytes,
|
|
314
|
+
sha256: result.sha256,
|
|
315
|
+
chunkIndex: result.chunkIndex,
|
|
316
|
+
chunkCount: result.chunkCount,
|
|
317
|
+
expiresAt: result.expiresAt,
|
|
318
|
+
}
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Deliberately read one exact storage value without retries, budgeting, or
|
|
324
|
+
* value-derived delivery metrics.
|
|
325
|
+
*
|
|
326
|
+
* @param {{ source: 'local_storage' | 'session_storage', key: string, tabId?: number, destinationId?: string }} args
|
|
327
|
+
* @returns {Promise<ToolResult>}
|
|
328
|
+
*/
|
|
329
|
+
export async function handleSensitiveReadTool(args) {
|
|
330
|
+
return withToolClient(
|
|
331
|
+
async (client) => {
|
|
332
|
+
const response = await requestBridgeWithRetry(
|
|
333
|
+
client,
|
|
334
|
+
'sensitive.read',
|
|
335
|
+
{ source: args.source, key: args.key },
|
|
336
|
+
{
|
|
337
|
+
tabId: typeof args.tabId === 'number' ? args.tabId : null,
|
|
338
|
+
source: REQUEST_SOURCE,
|
|
339
|
+
tokenBudget: null,
|
|
340
|
+
}
|
|
341
|
+
);
|
|
342
|
+
return createSensitiveReadResult(response);
|
|
343
|
+
},
|
|
344
|
+
{ destinationId: args.destinationId ?? null }
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
194
348
|
/**
|
|
195
349
|
* @param {{ calls?: Array<{ method?: string, params?: Record<string, unknown>, tabId?: number, destinationId?: string, budgetPreset?: 'quick' | 'normal' | 'deep' }> }} args
|
|
196
350
|
* @returns {Promise<ToolResult>}
|
|
@@ -354,6 +508,14 @@ export async function handleRawCallTool(args) {
|
|
|
354
508
|
if (!METHOD_SET.has(/** @type {BridgeMethod} */ (args.method))) {
|
|
355
509
|
return summarizeToolError(`Unknown bridge method "${args.method}".`);
|
|
356
510
|
}
|
|
511
|
+
if (args.method === 'protocol.describe') {
|
|
512
|
+
try {
|
|
513
|
+
const result = describeBridgeMethods(args.params);
|
|
514
|
+
return createToolResult('Bridge protocol description loaded.', result);
|
|
515
|
+
} catch (error) {
|
|
516
|
+
return summarizeToolError(error);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
357
519
|
return withToolClient(
|
|
358
520
|
async (client) => {
|
|
359
521
|
const method = /** @type {BridgeMethod} */ (args.method);
|
|
@@ -361,17 +523,128 @@ export async function handleRawCallTool(args) {
|
|
|
361
523
|
const response = await requestBridgeWithRetry(client, method, params, {
|
|
362
524
|
tabId: typeof args.tabId === 'number' ? args.tabId : null,
|
|
363
525
|
source: REQUEST_SOURCE,
|
|
364
|
-
tokenBudget:
|
|
526
|
+
tokenBudget:
|
|
527
|
+
method.startsWith('screenshot.') || method === 'sensitive.read'
|
|
528
|
+
? null
|
|
529
|
+
: getToolTokenBudget(args),
|
|
365
530
|
});
|
|
366
531
|
if (response.ok && method.startsWith('screenshot.')) {
|
|
367
532
|
return createScreenshotResult(response, method);
|
|
368
533
|
}
|
|
534
|
+
if (method === 'network.export_har') {
|
|
535
|
+
return createHarExportResult(response);
|
|
536
|
+
}
|
|
537
|
+
if (method === 'sensitive.read') {
|
|
538
|
+
return createSensitiveReadResult(response);
|
|
539
|
+
}
|
|
369
540
|
return summarizeToolResponse(response, method, params);
|
|
370
541
|
},
|
|
371
542
|
{ destinationId: args.destinationId ?? null }
|
|
372
543
|
);
|
|
373
544
|
}
|
|
374
545
|
|
|
546
|
+
/**
|
|
547
|
+
* Preserve the atomic HAR payload exactly. Generic evidence bounding must not
|
|
548
|
+
* turn a valid HAR document into a structurally invalid partial document.
|
|
549
|
+
*
|
|
550
|
+
* @param {import('../../protocol/src/types.js').BridgeResponse} response
|
|
551
|
+
* @returns {ToolResult}
|
|
552
|
+
*/
|
|
553
|
+
export function createHarExportResult(response) {
|
|
554
|
+
if (!response.ok) return summarizeToolResponse(response, 'network.export_har', {});
|
|
555
|
+
const result =
|
|
556
|
+
response.result && typeof response.result === 'object' && !Array.isArray(response.result)
|
|
557
|
+
? /** @type {Record<string, unknown>} */ (response.result)
|
|
558
|
+
: {};
|
|
559
|
+
const numericFields = [
|
|
560
|
+
'byteLength',
|
|
561
|
+
'entryCount',
|
|
562
|
+
'totalEntries',
|
|
563
|
+
'dropped',
|
|
564
|
+
'abandoned',
|
|
565
|
+
'inflight',
|
|
566
|
+
];
|
|
567
|
+
const captureState = result.captureState;
|
|
568
|
+
const validMetadata =
|
|
569
|
+
result.format === 'har' &&
|
|
570
|
+
result.harVersion === '1.2' &&
|
|
571
|
+
result.mimeType === 'application/json' &&
|
|
572
|
+
numericFields.every((field) => typeof result[field] === 'number') &&
|
|
573
|
+
(typeof result.startedAt === 'number' || result.startedAt === null) &&
|
|
574
|
+
(captureState === 'armed' ||
|
|
575
|
+
captureState === 'stop_failed' ||
|
|
576
|
+
captureState === 'stopped' ||
|
|
577
|
+
captureState === 'instrumented') &&
|
|
578
|
+
typeof result.truncated === 'boolean' &&
|
|
579
|
+
result.truncation !== null &&
|
|
580
|
+
typeof result.truncation === 'object';
|
|
581
|
+
if (!validMetadata) {
|
|
582
|
+
return summarizeToolError('network.export_har returned invalid HAR metadata.');
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
if (result.delivery === 'inline') {
|
|
586
|
+
if (!isHarDocument(result.har)) {
|
|
587
|
+
return summarizeToolError('network.export_har returned an invalid inline HAR document.');
|
|
588
|
+
}
|
|
589
|
+
} else if (result.delivery === 'artifact') {
|
|
590
|
+
const artifact =
|
|
591
|
+
result.artifact && typeof result.artifact === 'object' && !Array.isArray(result.artifact)
|
|
592
|
+
? /** @type {Record<string, unknown>} */ (result.artifact)
|
|
593
|
+
: {};
|
|
594
|
+
if (
|
|
595
|
+
artifact.kind !== 'har' ||
|
|
596
|
+
artifact.mimeType !== 'application/json' ||
|
|
597
|
+
typeof artifact.artifactId !== 'string' ||
|
|
598
|
+
typeof artifact.byteLength !== 'number' ||
|
|
599
|
+
typeof artifact.sha256 !== 'string' ||
|
|
600
|
+
!/^[a-f0-9]{64}$/u.test(artifact.sha256) ||
|
|
601
|
+
typeof artifact.chunkSize !== 'number' ||
|
|
602
|
+
typeof artifact.chunkCount !== 'number' ||
|
|
603
|
+
typeof artifact.createdAt !== 'string' ||
|
|
604
|
+
typeof artifact.expiresAt !== 'string'
|
|
605
|
+
) {
|
|
606
|
+
return summarizeToolError('network.export_har returned invalid artifact metadata.');
|
|
607
|
+
}
|
|
608
|
+
} else {
|
|
609
|
+
return summarizeToolError('network.export_har returned an invalid delivery mode.');
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const summary = summarizeBridgeResponse(response, 'network.export_har');
|
|
613
|
+
return createToolResult(summary.summary, result);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* @param {import('../../protocol/src/types.js').BridgeResponse} response
|
|
618
|
+
* @returns {ToolResult}
|
|
619
|
+
*/
|
|
620
|
+
function createSensitiveReadResult(response) {
|
|
621
|
+
if (!response.ok) return summarizeToolResponse(response, 'sensitive.read', {});
|
|
622
|
+
const result =
|
|
623
|
+
response.result && typeof response.result === 'object'
|
|
624
|
+
? /** @type {Record<string, unknown>} */ (response.result)
|
|
625
|
+
: {};
|
|
626
|
+
if (
|
|
627
|
+
(result.source !== 'local_storage' && result.source !== 'session_storage') ||
|
|
628
|
+
typeof result.value !== 'string' ||
|
|
629
|
+
result.exact !== true
|
|
630
|
+
) {
|
|
631
|
+
return summarizeToolError('sensitive.read returned an invalid exact-value response.');
|
|
632
|
+
}
|
|
633
|
+
return {
|
|
634
|
+
content: [
|
|
635
|
+
{
|
|
636
|
+
type: 'text',
|
|
637
|
+
text: 'Sensitive storage read succeeded. The exact value is available in structuredContent.value.',
|
|
638
|
+
},
|
|
639
|
+
],
|
|
640
|
+
structuredContent: {
|
|
641
|
+
source: result.source,
|
|
642
|
+
value: result.value,
|
|
643
|
+
exact: true,
|
|
644
|
+
},
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
|
|
375
648
|
/**
|
|
376
649
|
* @typedef {{ method: BridgeMethod, params: (args: Record<string, unknown>) => Record<string, unknown> }} InvestigateStep
|
|
377
650
|
*/
|