@agenttool/browser 0.1.0 → 0.3.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/CLAUDE.md +67 -12
- package/README.md +188 -31
- package/dist/src/browser.d.ts +27 -2
- package/dist/src/browser.d.ts.map +1 -1
- package/dist/src/browser.js +413 -98
- package/dist/src/browser.js.map +1 -1
- package/dist/src/capabilities.d.ts +57 -0
- package/dist/src/capabilities.d.ts.map +1 -0
- package/dist/src/capabilities.js +125 -0
- package/dist/src/capabilities.js.map +1 -0
- package/dist/src/cli.d.ts +1 -1
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +69 -15
- package/dist/src/cli.js.map +1 -1
- package/dist/src/config.d.ts +5 -0
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/config.js +84 -2
- package/dist/src/config.js.map +1 -1
- package/dist/src/index.d.ts +6 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcp.d.ts +14 -7
- package/dist/src/mcp.d.ts.map +1 -1
- package/dist/src/mcp.js +89 -66
- package/dist/src/mcp.js.map +1 -1
- package/dist/src/planning.d.ts +29 -0
- package/dist/src/planning.d.ts.map +1 -0
- package/dist/src/planning.js +107 -0
- package/dist/src/planning.js.map +1 -0
- package/dist/src/policy.d.ts +16 -6
- package/dist/src/policy.d.ts.map +1 -1
- package/dist/src/policy.js +72 -18
- package/dist/src/policy.js.map +1 -1
- package/dist/src/snapshot.d.ts +6 -0
- package/dist/src/snapshot.d.ts.map +1 -1
- package/dist/src/snapshot.js +116 -23
- package/dist/src/snapshot.js.map +1 -1
- package/dist/src/types.d.ts +18 -5
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js.map +1 -1
- package/dist/src/version.d.ts +6 -0
- package/dist/src/version.d.ts.map +1 -0
- package/dist/src/version.js +6 -0
- package/dist/src/version.js.map +1 -0
- package/package.json +2 -2
package/dist/src/browser.js
CHANGED
|
@@ -3,9 +3,12 @@ import { existsSync } from "node:fs";
|
|
|
3
3
|
import { chmod, lstat, mkdir, realpath } from "node:fs/promises";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
6
|
+
import { performance } from "node:perf_hooks";
|
|
7
|
+
import { resolveBrowserCapabilities, } from "./capabilities.js";
|
|
6
8
|
import { asBrowserError, BrowserError } from "./errors.js";
|
|
9
|
+
import { planBrowserAction, } from "./planning.js";
|
|
7
10
|
import { BrowserNetworkPolicy, redactHtmlUrlAttributes, redactUrlReferenceForOutput, redactUrlForOutput, redactUrlsInText, } from "./policy.js";
|
|
8
|
-
import { boundText, compactAriaSnapshot, intersectsViewport, looksLikeSensitiveControl, parseAriaCandidates, redactAriaSecrets, redactSensitiveInputValues, } from "./snapshot.js";
|
|
11
|
+
import { boundText, compactAriaSnapshot, intersectsViewport, looksLikeSensitiveControl, parseAriaCandidates, parseStructuralAriaCandidates, redactAriaSecrets, redactSensitiveInputValues, } from "./snapshot.js";
|
|
9
12
|
import { OBSERVATION_SCHEMA, } from "./types.js";
|
|
10
13
|
export const DEFAULT_BROWSER_LIMITS = Object.freeze({
|
|
11
14
|
maxSnapshotChars: 24_000,
|
|
@@ -20,6 +23,12 @@ const DEFAULT_VIEWPORT = Object.freeze({ width: 1280, height: 720 });
|
|
|
20
23
|
const DEFAULT_ACTION_TIMEOUT_MS = 10_000;
|
|
21
24
|
const DEFAULT_NAVIGATION_TIMEOUT_MS = 30_000;
|
|
22
25
|
const MAX_RESPONSE_HINT_CHARS = 4_096;
|
|
26
|
+
const MAX_RETAINED_SNAPSHOTS_PER_TAB = 8;
|
|
27
|
+
const MAX_STRUCTURAL_CONTEXT_ELEMENTS = 32;
|
|
28
|
+
const VIEWPORT_SETTLE_INTERVAL_MS = 25;
|
|
29
|
+
const VIEWPORT_SETTLE_MAX_MS = 1_000;
|
|
30
|
+
const VIEWPORT_SETTLE_STABLE_INTERVALS = 2;
|
|
31
|
+
const VIEWPORT_SETTLE_TOLERANCE_PX = 0.5;
|
|
23
32
|
const RESPONSE_HINT_HEADERS = Object.freeze([
|
|
24
33
|
"link",
|
|
25
34
|
"content-location",
|
|
@@ -39,9 +48,12 @@ export class AgentBrowser {
|
|
|
39
48
|
browser;
|
|
40
49
|
states = new Map();
|
|
41
50
|
pageStates = new Map();
|
|
51
|
+
requestPolicyDenials = new Map();
|
|
42
52
|
activeTabId = null;
|
|
43
53
|
nextTabNumber = 1;
|
|
54
|
+
requestPolicyDenialSequence = 0;
|
|
44
55
|
closed = false;
|
|
56
|
+
closePromise = null;
|
|
45
57
|
operationTail = Promise.resolve();
|
|
46
58
|
constructor(options, context, browser) {
|
|
47
59
|
this.sessionId = `session_${randomUUID()}`;
|
|
@@ -49,8 +61,7 @@ export class AgentBrowser {
|
|
|
49
61
|
this.context = context;
|
|
50
62
|
this.browser = browser;
|
|
51
63
|
this.policy = new BrowserNetworkPolicy({
|
|
52
|
-
|
|
53
|
-
allowLocalNetwork: options.allowLocalNetwork,
|
|
64
|
+
capabilities: options.capabilities,
|
|
54
65
|
...(options.resolveHostname
|
|
55
66
|
? { resolveHostname: options.resolveHostname }
|
|
56
67
|
: {}),
|
|
@@ -79,7 +90,7 @@ export class AgentBrowser {
|
|
|
79
90
|
viewport: normalized.viewport,
|
|
80
91
|
acceptDownloads: false,
|
|
81
92
|
ignoreHTTPSErrors: false,
|
|
82
|
-
serviceWorkers:
|
|
93
|
+
serviceWorkers: normalized.capabilities.runtime.serviceWorkers,
|
|
83
94
|
};
|
|
84
95
|
if (normalized.profile.mode === "persistent") {
|
|
85
96
|
context = await runtime.launchPersistentContext(normalized.profile.directory, { ...launchOptions, ...contextOptions });
|
|
@@ -101,13 +112,7 @@ export class AgentBrowser {
|
|
|
101
112
|
}
|
|
102
113
|
catch (error) {
|
|
103
114
|
try {
|
|
104
|
-
await context
|
|
105
|
-
}
|
|
106
|
-
catch {
|
|
107
|
-
// Preserve the launch error; cleanup failure is secondary.
|
|
108
|
-
}
|
|
109
|
-
try {
|
|
110
|
-
await browser?.close();
|
|
115
|
+
await closeRuntime(browser, context);
|
|
111
116
|
}
|
|
112
117
|
catch {
|
|
113
118
|
// Preserve the launch error; cleanup failure is secondary.
|
|
@@ -122,9 +127,12 @@ export class AgentBrowser {
|
|
|
122
127
|
async openUnlocked(url) {
|
|
123
128
|
this.assertOpen();
|
|
124
129
|
const destination = await this.policy.assertAllowed(url);
|
|
130
|
+
this.assertOpen();
|
|
125
131
|
const page = await this.context.newPage();
|
|
132
|
+
this.assertOpen();
|
|
126
133
|
const state = this.registerPage(page);
|
|
127
134
|
this.activeTabId = state.id;
|
|
135
|
+
const denialSequence = this.requestPolicyDenialSequence;
|
|
128
136
|
try {
|
|
129
137
|
this.clearMainDocumentResponse(state);
|
|
130
138
|
const response = await page.goto(destination.href, {
|
|
@@ -132,9 +140,15 @@ export class AgentBrowser {
|
|
|
132
140
|
timeout: this.options.navigationTimeoutMs,
|
|
133
141
|
});
|
|
134
142
|
this.queueMainDocumentResponse(state, response);
|
|
143
|
+
const denial = this.requestPolicyDenialAfter(denialSequence, state.id);
|
|
144
|
+
if (denial)
|
|
145
|
+
throw denial;
|
|
135
146
|
}
|
|
136
147
|
catch (error) {
|
|
137
148
|
this.invalidate(state);
|
|
149
|
+
const denial = this.requestPolicyDenialAfter(denialSequence, state.id);
|
|
150
|
+
if (denial)
|
|
151
|
+
throw denial;
|
|
138
152
|
throw asBrowserError(error, "action_failed", "Navigation was attempted once and did not complete.");
|
|
139
153
|
}
|
|
140
154
|
return this.observeUnlocked({ tabId: state.id });
|
|
@@ -151,8 +165,12 @@ export class AgentBrowser {
|
|
|
151
165
|
}
|
|
152
166
|
const state = this.getState(options.tabId);
|
|
153
167
|
this.activeTabId = state.id;
|
|
154
|
-
this.
|
|
155
|
-
|
|
168
|
+
await this.awaitWindowViewportSettled(state.page);
|
|
169
|
+
this.assertOpen();
|
|
170
|
+
state.revision += 1;
|
|
171
|
+
const observationRevision = state.revision;
|
|
172
|
+
const observationNavigationEpoch = state.navigationEpoch;
|
|
173
|
+
const snapshotId = `${this.sessionId}:${state.id}:${observationRevision}`;
|
|
156
174
|
const viewport = state.page.viewportSize() ?? this.options.viewport;
|
|
157
175
|
const includeText = options.includeText ?? true;
|
|
158
176
|
const maxTextChars = boundedPositiveInteger(options.maxTextChars, this.options.limits.maxTextChars, "maxTextChars", this.options.limits.maxTextChars);
|
|
@@ -163,39 +181,63 @@ export class AgentBrowser {
|
|
|
163
181
|
timeout: this.options.actionTimeoutMs,
|
|
164
182
|
});
|
|
165
183
|
const candidates = parseAriaCandidates(raw).slice(0, this.options.limits.maxSnapshotElements * 3);
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
184
|
+
const structuralCandidateScanLimit = MAX_STRUCTURAL_CONTEXT_ELEMENTS * 3;
|
|
185
|
+
const allStructuralCandidates = parseStructuralAriaCandidates(raw);
|
|
186
|
+
const structuralCandidates = allStructuralCandidates.slice(0, structuralCandidateScanLimit);
|
|
187
|
+
const [inspected, inspectedStructural] = await Promise.all([
|
|
188
|
+
Promise.all(candidates.map(async (candidate) => {
|
|
189
|
+
const locator = state.page.locator(`aria-ref=${candidate.nativeRef}`);
|
|
190
|
+
try {
|
|
191
|
+
if ((await locator.count()) !== 1 || !(await locator.isVisible())) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
if (!intersectsViewport(await locator.boundingBox(), viewport)) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
const attributeNames = [
|
|
198
|
+
"type",
|
|
199
|
+
"autocomplete",
|
|
200
|
+
"name",
|
|
201
|
+
"id",
|
|
202
|
+
"placeholder",
|
|
203
|
+
"aria-label",
|
|
204
|
+
];
|
|
205
|
+
const values = await Promise.all(attributeNames.map((name) => locator.getAttribute(name)));
|
|
206
|
+
const attributes = {};
|
|
207
|
+
attributeNames.forEach((name, index) => {
|
|
208
|
+
attributes[name] = values[index] ?? null;
|
|
209
|
+
});
|
|
210
|
+
return {
|
|
211
|
+
candidate,
|
|
212
|
+
secret: looksLikeSensitiveControl(attributes, candidate.name),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
// The page changed while it was being observed. Omit that target;
|
|
217
|
+
// never guess a replacement ref.
|
|
170
218
|
return null;
|
|
171
219
|
}
|
|
172
|
-
|
|
220
|
+
})),
|
|
221
|
+
Promise.all(structuralCandidates.map(async (candidate) => {
|
|
222
|
+
const locator = state.page.locator(`aria-ref=${candidate.nativeRef}`);
|
|
223
|
+
try {
|
|
224
|
+
if ((await locator.count()) !== 1 || !(await locator.isVisible())) {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
if (!intersectsViewport(await locator.boundingBox(), viewport)) {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
return candidate.nativeRef;
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
// Structural context is optional. Omit a node whose geometry
|
|
234
|
+
// changed rather than exposing an unbound or guessed context.
|
|
173
235
|
return null;
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
"name",
|
|
178
|
-
"id",
|
|
179
|
-
"placeholder",
|
|
180
|
-
"aria-label",
|
|
181
|
-
];
|
|
182
|
-
const values = await Promise.all(attributeNames.map((name) => locator.getAttribute(name)));
|
|
183
|
-
const attributes = {};
|
|
184
|
-
attributeNames.forEach((name, index) => {
|
|
185
|
-
attributes[name] = values[index] ?? null;
|
|
186
|
-
});
|
|
187
|
-
return {
|
|
188
|
-
candidate,
|
|
189
|
-
secret: looksLikeSensitiveControl(attributes, candidate.name),
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
catch {
|
|
193
|
-
// The page changed while it was being observed. Omit that target;
|
|
194
|
-
// never guess a replacement ref.
|
|
195
|
-
return null;
|
|
196
|
-
}
|
|
197
|
-
}));
|
|
236
|
+
}
|
|
237
|
+
})),
|
|
238
|
+
]);
|
|
198
239
|
const visibleRefs = new Set();
|
|
240
|
+
const visibleStructuralRefs = new Set();
|
|
199
241
|
const secretRefs = new Set();
|
|
200
242
|
const publicRefs = new Map();
|
|
201
243
|
for (const item of inspected) {
|
|
@@ -207,16 +249,21 @@ export class AgentBrowser {
|
|
|
207
249
|
if (item.secret)
|
|
208
250
|
secretRefs.add(item.candidate.nativeRef);
|
|
209
251
|
}
|
|
252
|
+
for (const nativeRef of inspectedStructural) {
|
|
253
|
+
if (nativeRef)
|
|
254
|
+
visibleStructuralRefs.add(nativeRef);
|
|
255
|
+
}
|
|
210
256
|
const sanitizedRaw = redactUrlsInText(redactAriaSecrets(raw, secretRefs));
|
|
211
257
|
const compact = compactAriaSnapshot(sanitizedRaw, {
|
|
212
258
|
publicRefs,
|
|
213
259
|
visibleRefs,
|
|
260
|
+
visibleStructuralRefs,
|
|
214
261
|
secretRefs,
|
|
215
262
|
maxChars: this.options.limits.maxSnapshotChars,
|
|
216
263
|
maxElements: this.options.limits.maxSnapshotElements,
|
|
264
|
+
maxStructuralElements: MAX_STRUCTURAL_CONTEXT_ELEMENTS,
|
|
217
265
|
});
|
|
218
|
-
|
|
219
|
-
state.refs = new Map(compact.refs.map((ref) => {
|
|
266
|
+
const snapshotRefs = new Map(compact.refs.map((ref) => {
|
|
220
267
|
const nativeRef = publicRefsEntries(publicRefs, ref.ref);
|
|
221
268
|
return [ref.ref, nativeRef];
|
|
222
269
|
}));
|
|
@@ -240,13 +287,15 @@ export class AgentBrowser {
|
|
|
240
287
|
: null;
|
|
241
288
|
if (responseSequence !== state.responseSequence)
|
|
242
289
|
response = null;
|
|
290
|
+
this.assertObservationCurrent(state, observationRevision, observationNavigationEpoch);
|
|
291
|
+
this.rememberSnapshot(state, snapshotId, snapshotRefs, observationNavigationEpoch);
|
|
243
292
|
return {
|
|
244
293
|
schema: OBSERVATION_SCHEMA,
|
|
245
294
|
sessionId: this.sessionId,
|
|
246
295
|
snapshotId,
|
|
247
296
|
tabId: state.id,
|
|
248
297
|
pageId: state.id,
|
|
249
|
-
revision:
|
|
298
|
+
revision: observationRevision,
|
|
250
299
|
url,
|
|
251
300
|
title,
|
|
252
301
|
snapshot: compact.snapshot,
|
|
@@ -254,7 +303,8 @@ export class AgentBrowser {
|
|
|
254
303
|
refs: compact.refs,
|
|
255
304
|
response,
|
|
256
305
|
truncated: {
|
|
257
|
-
snapshot: compact.truncated.snapshot
|
|
306
|
+
snapshot: compact.truncated.snapshot
|
|
307
|
+
|| allStructuralCandidates.length > structuralCandidateScanLimit,
|
|
258
308
|
text: textTruncated,
|
|
259
309
|
elements: compact.truncated.elements
|
|
260
310
|
|| candidates.length
|
|
@@ -265,24 +315,37 @@ export class AgentBrowser {
|
|
|
265
315
|
};
|
|
266
316
|
}
|
|
267
317
|
catch (error) {
|
|
268
|
-
state.snapshotId = null;
|
|
269
|
-
state.refs.clear();
|
|
270
318
|
throw asBrowserError(error, "action_failed", "Could not create a bounded browser observation.");
|
|
271
319
|
}
|
|
272
320
|
}
|
|
273
321
|
async act(action) {
|
|
274
|
-
|
|
322
|
+
const capturedAction = captureBrowserAction(action);
|
|
323
|
+
return this.withLock(() => this.actUnlocked(capturedAction));
|
|
324
|
+
}
|
|
325
|
+
/** Return the immutable authority manifest selected when this session launched. */
|
|
326
|
+
capabilities() {
|
|
327
|
+
return this.options.capabilities;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Forecast possible consequences without touching the page or executing the
|
|
331
|
+
* action. Planning is advisory and never widens launch-time authority.
|
|
332
|
+
*/
|
|
333
|
+
plan(action) {
|
|
334
|
+
this.assertOpen();
|
|
335
|
+
validateAction(action, this.options);
|
|
336
|
+
return planBrowserAction(action, this.options.capabilities);
|
|
275
337
|
}
|
|
276
338
|
/**
|
|
277
339
|
* Atomic convenience for process adapters: no other session operation can
|
|
278
340
|
* interleave between the one action attempt and its read-only observation.
|
|
279
341
|
*/
|
|
280
342
|
async actAndObserve(action) {
|
|
343
|
+
const capturedAction = captureBrowserAction(action);
|
|
281
344
|
return this.withLock(async () => {
|
|
282
|
-
const actionResult = await this.actUnlocked(
|
|
345
|
+
const actionResult = await this.actUnlocked(capturedAction);
|
|
283
346
|
try {
|
|
284
347
|
const observation = await this.observeUnlocked({
|
|
285
|
-
...(
|
|
348
|
+
...(capturedAction.kind !== "close_tab" && actionResult.tabId
|
|
286
349
|
? { tabId: actionResult.tabId }
|
|
287
350
|
: {}),
|
|
288
351
|
});
|
|
@@ -316,15 +379,20 @@ export class AgentBrowser {
|
|
|
316
379
|
return this.closeTab(state);
|
|
317
380
|
let resolved = null;
|
|
318
381
|
if ("ref" in action && typeof action.ref === "string") {
|
|
319
|
-
resolved = await this.resolveRef(state, action.ref, action.snapshotId, action.kind !== "scroll");
|
|
382
|
+
resolved = await this.resolveRef(state, action.ref, action.snapshotId, action.kind !== "scroll", true);
|
|
320
383
|
}
|
|
321
|
-
|
|
322
|
-
await this.policy.assertAllowed(action.url)
|
|
384
|
+
const navigationDestination = action.kind === "navigate"
|
|
385
|
+
? await this.policy.assertAllowed(action.url)
|
|
386
|
+
: null;
|
|
387
|
+
this.assertOpen();
|
|
388
|
+
const denialSequence = this.requestPolicyDenialSequence;
|
|
323
389
|
try {
|
|
390
|
+
if (resolved)
|
|
391
|
+
this.assertResolvedRefCurrent(resolved);
|
|
324
392
|
switch (action.kind) {
|
|
325
393
|
case "navigate":
|
|
326
394
|
this.clearMainDocumentResponse(state);
|
|
327
|
-
this.queueMainDocumentResponse(state, await state.page.goto(
|
|
395
|
+
this.queueMainDocumentResponse(state, await state.page.goto(navigationDestination.href, {
|
|
328
396
|
waitUntil: "domcontentloaded",
|
|
329
397
|
timeout: this.options.navigationTimeoutMs,
|
|
330
398
|
}));
|
|
@@ -377,8 +445,14 @@ export class AgentBrowser {
|
|
|
377
445
|
default:
|
|
378
446
|
throw new BrowserError("invalid_action", "Unsupported browser action.");
|
|
379
447
|
}
|
|
448
|
+
const denial = this.requestPolicyDenialAfter(denialSequence, state.id);
|
|
449
|
+
if (denial)
|
|
450
|
+
throw denial;
|
|
380
451
|
}
|
|
381
452
|
catch (error) {
|
|
453
|
+
const denial = this.requestPolicyDenialAfter(denialSequence, state.id);
|
|
454
|
+
if (denial)
|
|
455
|
+
throw denial;
|
|
382
456
|
throw asBrowserError(error, "action_failed", "Browser action was attempted once and did not complete.");
|
|
383
457
|
}
|
|
384
458
|
finally {
|
|
@@ -393,9 +467,12 @@ export class AgentBrowser {
|
|
|
393
467
|
this.assertOpen();
|
|
394
468
|
validateExtractInput(input, this.options.limits.maxExtractChars);
|
|
395
469
|
const state = this.getState(input.tabId);
|
|
470
|
+
const refTargeted = "ref" in input && typeof input.ref === "string";
|
|
471
|
+
let resolved = null;
|
|
396
472
|
let locator;
|
|
397
|
-
if (
|
|
398
|
-
|
|
473
|
+
if (refTargeted) {
|
|
474
|
+
resolved = await this.resolveRef(state, input.ref, input.snapshotId, false, false);
|
|
475
|
+
locator = resolved.locator;
|
|
399
476
|
}
|
|
400
477
|
else {
|
|
401
478
|
locator = state.page.locator(input.selector ?? "body");
|
|
@@ -404,16 +481,25 @@ export class AgentBrowser {
|
|
|
404
481
|
const rawUrl = state.page.url();
|
|
405
482
|
try {
|
|
406
483
|
if (input.format === "links") {
|
|
407
|
-
const
|
|
408
|
-
|
|
484
|
+
const selfLinkLocator = refTargeted
|
|
485
|
+
? locator.locator("xpath=self::*[local-name()='a'][@href]")
|
|
486
|
+
: null;
|
|
487
|
+
const selfLinkCount = selfLinkLocator
|
|
488
|
+
? Math.min(await selfLinkLocator.count(), 1)
|
|
489
|
+
: 0;
|
|
490
|
+
const descendantLinkLocator = locator.locator("a[href]");
|
|
491
|
+
const descendantLinkCount = await descendantLinkLocator.count();
|
|
492
|
+
const count = selfLinkCount + descendantLinkCount;
|
|
409
493
|
const links = [];
|
|
410
494
|
let usedChars = 0;
|
|
411
495
|
let truncated = count > this.options.limits.maxExtractLinks;
|
|
412
496
|
const limit = Math.min(count, this.options.limits.maxExtractLinks);
|
|
413
497
|
for (let index = 0; index < limit; index += 1) {
|
|
414
|
-
const link =
|
|
498
|
+
const link = index < selfLinkCount
|
|
499
|
+
? selfLinkLocator.nth(index)
|
|
500
|
+
: descendantLinkLocator.nth(index - selfLinkCount);
|
|
415
501
|
const href = await link.getAttribute("href");
|
|
416
|
-
if (
|
|
502
|
+
if (href === null)
|
|
417
503
|
continue;
|
|
418
504
|
const text = redactUrlsInText((await link.textContent())?.trim() ?? "");
|
|
419
505
|
const resolvedHref = redactUrlForOutput(safeResolveUrl(href, rawUrl));
|
|
@@ -425,6 +511,9 @@ export class AgentBrowser {
|
|
|
425
511
|
usedChars += chars;
|
|
426
512
|
links.push({ text, href: resolvedHref });
|
|
427
513
|
}
|
|
514
|
+
if (resolved)
|
|
515
|
+
this.assertResolvedRefCurrent(resolved);
|
|
516
|
+
this.assertOpen();
|
|
428
517
|
return {
|
|
429
518
|
format: input.format,
|
|
430
519
|
sessionId: this.sessionId,
|
|
@@ -442,6 +531,9 @@ export class AgentBrowser {
|
|
|
442
531
|
? redactHtmlUrlAttributes(redactSensitiveInputValues(await locator.innerHTML()))
|
|
443
532
|
: await locator.innerText();
|
|
444
533
|
const bounded = boundText(redactUrlsInText(rawContent), maxChars);
|
|
534
|
+
if (resolved)
|
|
535
|
+
this.assertResolvedRefCurrent(resolved);
|
|
536
|
+
this.assertOpen();
|
|
445
537
|
return {
|
|
446
538
|
format: input.format,
|
|
447
539
|
sessionId: this.sessionId,
|
|
@@ -475,8 +567,11 @@ export class AgentBrowser {
|
|
|
475
567
|
const artifactPath = join(this.options.outputDir, `${timestamp}-${state.id}-${randomUUID()}.png`);
|
|
476
568
|
try {
|
|
477
569
|
await validateCanonicalStoragePaths(this.options.profile, this.options.outputDir);
|
|
570
|
+
this.assertOpen();
|
|
478
571
|
await ensurePrivateDirectory(this.options.outputDir, "artifact");
|
|
572
|
+
this.assertOpen();
|
|
479
573
|
await validateCanonicalStoragePaths(this.options.profile, this.options.outputDir);
|
|
574
|
+
this.assertOpen();
|
|
480
575
|
const bytes = await state.page.screenshot({
|
|
481
576
|
path: artifactPath,
|
|
482
577
|
fullPage,
|
|
@@ -484,6 +579,7 @@ export class AgentBrowser {
|
|
|
484
579
|
});
|
|
485
580
|
if (process.platform !== "win32")
|
|
486
581
|
await chmod(artifactPath, 0o600);
|
|
582
|
+
this.assertOpen();
|
|
487
583
|
const rawUrl = state.page.url();
|
|
488
584
|
return {
|
|
489
585
|
sessionId: this.sessionId,
|
|
@@ -519,42 +615,53 @@ export class AgentBrowser {
|
|
|
519
615
|
title: boundText(redactUrlsInText(await state.page.title()), 512).value,
|
|
520
616
|
active: state.id === this.activeTabId,
|
|
521
617
|
});
|
|
618
|
+
this.assertOpen();
|
|
522
619
|
}
|
|
523
620
|
return summaries;
|
|
524
621
|
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
async closeUnlocked() {
|
|
529
|
-
if (this.closed)
|
|
530
|
-
return;
|
|
622
|
+
close() {
|
|
623
|
+
if (this.closePromise)
|
|
624
|
+
return this.closePromise;
|
|
531
625
|
this.closed = true;
|
|
532
626
|
this.states.clear();
|
|
533
627
|
this.pageStates.clear();
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
await this.browser?.close();
|
|
539
|
-
}
|
|
628
|
+
this.requestPolicyDenials.clear();
|
|
629
|
+
const closing = Promise.resolve().then(() => closeRuntime(this.browser, this.context));
|
|
630
|
+
this.closePromise = closing;
|
|
631
|
+
return closing;
|
|
540
632
|
}
|
|
541
633
|
async installRequestPolicy() {
|
|
542
634
|
await this.context.route("**/*", async (route) => {
|
|
635
|
+
const request = route.request();
|
|
636
|
+
try {
|
|
637
|
+
await this.policy.assertAllowed(request.url());
|
|
638
|
+
}
|
|
639
|
+
catch (error) {
|
|
640
|
+
this.recordMainFrameRequestPolicyDenial(request, asBrowserError(error, "network_blocked", "Browser request was denied by the launch-time network policy."));
|
|
641
|
+
await route.abort("blockedbyclient");
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
543
644
|
try {
|
|
544
|
-
await this.policy.assertAllowed(route.request().url());
|
|
545
645
|
await route.continue();
|
|
546
646
|
}
|
|
547
647
|
catch {
|
|
548
648
|
await route.abort("blockedbyclient");
|
|
549
649
|
}
|
|
550
650
|
});
|
|
551
|
-
// HTTP routing does not cover WebSockets. V0 blocks every WebSocket
|
|
552
|
-
// connection instead of claiming the HTTP(S) DNS policy covers it.
|
|
553
651
|
await this.context.routeWebSocket("**/*", async (route) => {
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
652
|
+
try {
|
|
653
|
+
await this.policy.assertWebSocketAllowed(route.url());
|
|
654
|
+
if (typeof route.connectToServer !== "function") {
|
|
655
|
+
throw new BrowserError("invalid_options", "Browser runtime cannot pass an allowed WebSocket to its server.");
|
|
656
|
+
}
|
|
657
|
+
route.connectToServer();
|
|
658
|
+
}
|
|
659
|
+
catch {
|
|
660
|
+
await route.close({
|
|
661
|
+
code: 1008,
|
|
662
|
+
reason: "WebSocket destination is outside this browser authority",
|
|
663
|
+
});
|
|
664
|
+
}
|
|
558
665
|
});
|
|
559
666
|
}
|
|
560
667
|
refreshPages() {
|
|
@@ -562,6 +669,7 @@ export class AgentBrowser {
|
|
|
562
669
|
if (state.page.isClosed()) {
|
|
563
670
|
this.states.delete(state.id);
|
|
564
671
|
this.pageStates.delete(state.page);
|
|
672
|
+
this.requestPolicyDenials.delete(state.id);
|
|
565
673
|
}
|
|
566
674
|
}
|
|
567
675
|
for (const page of this.context.pages()) {
|
|
@@ -581,24 +689,28 @@ export class AgentBrowser {
|
|
|
581
689
|
id: `tab_${this.nextTabNumber++}`,
|
|
582
690
|
page,
|
|
583
691
|
revision: 0,
|
|
584
|
-
|
|
585
|
-
|
|
692
|
+
navigationEpoch: 0,
|
|
693
|
+
snapshots: new Map(),
|
|
586
694
|
response: null,
|
|
587
695
|
responseDocumentUrl: null,
|
|
588
696
|
responseCapture: Promise.resolve(),
|
|
589
697
|
responseSequence: 0,
|
|
590
698
|
};
|
|
699
|
+
this.watchPageEvents(state);
|
|
591
700
|
this.pageStates.set(page, state);
|
|
592
701
|
this.states.set(state.id, state);
|
|
593
702
|
this.activeTabId = state.id;
|
|
594
|
-
this.watchMainDocumentResponses(state);
|
|
595
703
|
return state;
|
|
596
704
|
}
|
|
597
|
-
|
|
705
|
+
watchPageEvents(state) {
|
|
598
706
|
if (typeof state.page.on !== "function"
|
|
599
707
|
|| typeof state.page.mainFrame !== "function") {
|
|
600
|
-
|
|
708
|
+
throw new BrowserError("invalid_options", "Browser runtime must support page frame-navigation events and main-frame identity.");
|
|
601
709
|
}
|
|
710
|
+
state.page.on("framenavigated", () => {
|
|
711
|
+
state.navigationEpoch += 1;
|
|
712
|
+
this.invalidate(state);
|
|
713
|
+
});
|
|
602
714
|
state.page.on("response", (response) => {
|
|
603
715
|
try {
|
|
604
716
|
const request = response.request();
|
|
@@ -717,17 +829,34 @@ export class AgentBrowser {
|
|
|
717
829
|
}
|
|
718
830
|
invalidate(state) {
|
|
719
831
|
state.revision += 1;
|
|
720
|
-
state.
|
|
721
|
-
|
|
832
|
+
state.snapshots.clear();
|
|
833
|
+
}
|
|
834
|
+
rememberSnapshot(state, snapshotId, refs, navigationEpoch) {
|
|
835
|
+
state.snapshots.set(snapshotId, { navigationEpoch, refs });
|
|
836
|
+
while (state.snapshots.size > MAX_RETAINED_SNAPSHOTS_PER_TAB) {
|
|
837
|
+
const oldest = state.snapshots.keys().next().value;
|
|
838
|
+
if (oldest === undefined)
|
|
839
|
+
break;
|
|
840
|
+
state.snapshots.delete(oldest);
|
|
841
|
+
}
|
|
722
842
|
}
|
|
723
|
-
|
|
843
|
+
assertObservationCurrent(state, revision, navigationEpoch) {
|
|
844
|
+
this.assertOpen();
|
|
845
|
+
if (state.revision !== revision
|
|
846
|
+
|| state.navigationEpoch !== navigationEpoch) {
|
|
847
|
+
throw new BrowserError("action_failed", "The page navigated while it was being observed; observe the tab again.");
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
async resolveRef(state, ref, snapshotId, requireEnabled, requireCurrentViewport) {
|
|
724
851
|
if (!snapshotId) {
|
|
725
852
|
throw new BrowserError("snapshot_required", "A snapshotId is required for every ref-targeted operation.");
|
|
726
853
|
}
|
|
727
|
-
|
|
854
|
+
const snapshot = state.snapshots.get(snapshotId);
|
|
855
|
+
if (!snapshot
|
|
856
|
+
|| snapshot.navigationEpoch !== state.navigationEpoch) {
|
|
728
857
|
throw new BrowserError("stale_snapshot", "The snapshot is stale; observe the tab again before acting.");
|
|
729
858
|
}
|
|
730
|
-
const nativeRef =
|
|
859
|
+
const nativeRef = snapshot.refs.get(ref);
|
|
731
860
|
if (!nativeRef) {
|
|
732
861
|
throw new BrowserError("ref_not_found", "The ref does not belong to this snapshot.");
|
|
733
862
|
}
|
|
@@ -742,16 +871,168 @@ export class AgentBrowser {
|
|
|
742
871
|
if (!(await locator.isVisible())) {
|
|
743
872
|
throw new BrowserError("ref_hidden", "The snapshot ref is no longer visible.");
|
|
744
873
|
}
|
|
874
|
+
if (requireCurrentViewport
|
|
875
|
+
&& !intersectsViewport(await locator.boundingBox(), state.page.viewportSize() ?? this.options.viewport)) {
|
|
876
|
+
throw new BrowserError("stale_snapshot", "The snapshot ref moved outside the current viewport; observe the tab again before acting.");
|
|
877
|
+
}
|
|
745
878
|
if (requireEnabled && !(await locator.isEnabled())) {
|
|
746
879
|
throw new BrowserError("ref_disabled", "The snapshot ref is disabled.");
|
|
747
880
|
}
|
|
748
|
-
|
|
881
|
+
const resolved = { state, locator, snapshotId, snapshot };
|
|
882
|
+
this.assertResolvedRefCurrent(resolved);
|
|
883
|
+
return resolved;
|
|
884
|
+
}
|
|
885
|
+
assertResolvedRefCurrent(resolved) {
|
|
886
|
+
this.assertOpen();
|
|
887
|
+
if (resolved.state.navigationEpoch !== resolved.snapshot.navigationEpoch
|
|
888
|
+
|| resolved.state.snapshots.get(resolved.snapshotId) !== resolved.snapshot) {
|
|
889
|
+
throw new BrowserError("stale_snapshot", "The snapshot is stale; observe the tab again before acting.");
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Playwright's wheel dispatch and a page's own smooth scrolling can outlive
|
|
894
|
+
* the action promise. Sample top-level viewport geometry before issuing refs
|
|
895
|
+
* so the observation is less likely to describe an in-flight window scroll.
|
|
896
|
+
* This is deliberately best-effort: a probe failure must not turn an already
|
|
897
|
+
* executed action into a reported action failure.
|
|
898
|
+
*/
|
|
899
|
+
async awaitWindowViewportSettled(page) {
|
|
900
|
+
try {
|
|
901
|
+
const documentElement = page.locator("html");
|
|
902
|
+
const deadline = performance.now() + VIEWPORT_SETTLE_MAX_MS;
|
|
903
|
+
const remainingTimeout = () => {
|
|
904
|
+
const remaining = deadline - performance.now();
|
|
905
|
+
return remaining > 0 ? Math.max(1, Math.ceil(remaining)) : null;
|
|
906
|
+
};
|
|
907
|
+
const initialTimeout = remainingTimeout();
|
|
908
|
+
if (initialTimeout === null)
|
|
909
|
+
return;
|
|
910
|
+
let previous = await documentElement.boundingBox({
|
|
911
|
+
timeout: initialTimeout,
|
|
912
|
+
});
|
|
913
|
+
if (!previous)
|
|
914
|
+
return;
|
|
915
|
+
let stableIntervals = 0;
|
|
916
|
+
while (true) {
|
|
917
|
+
const remainingBeforeWait = deadline - performance.now();
|
|
918
|
+
if (remainingBeforeWait <= 0)
|
|
919
|
+
return;
|
|
920
|
+
await page.waitForTimeout(Math.min(VIEWPORT_SETTLE_INTERVAL_MS, remainingBeforeWait));
|
|
921
|
+
const probeTimeout = remainingTimeout();
|
|
922
|
+
if (probeTimeout === null)
|
|
923
|
+
return;
|
|
924
|
+
const current = await documentElement.boundingBox({
|
|
925
|
+
timeout: probeTimeout,
|
|
926
|
+
});
|
|
927
|
+
if (!current)
|
|
928
|
+
return;
|
|
929
|
+
if (Math.abs(current.x - previous.x) <= VIEWPORT_SETTLE_TOLERANCE_PX
|
|
930
|
+
&& Math.abs(current.y - previous.y) <= VIEWPORT_SETTLE_TOLERANCE_PX) {
|
|
931
|
+
stableIntervals += 1;
|
|
932
|
+
if (stableIntervals >= VIEWPORT_SETTLE_STABLE_INTERVALS)
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
else {
|
|
936
|
+
stableIntervals = 0;
|
|
937
|
+
}
|
|
938
|
+
previous = current;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
catch {
|
|
942
|
+
// Observation remains available when a custom runtime cannot expose
|
|
943
|
+
// stable document geometry or the page changes during this probe.
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
recordMainFrameRequestPolicyDenial(request, error) {
|
|
947
|
+
try {
|
|
948
|
+
if (!request.isNavigationRequest())
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
catch {
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
// A popup's initial navigation can be routed before Playwright has created
|
|
955
|
+
// a Frame object or exposed the Page through context.pages(). Such a
|
|
956
|
+
// request is still a navigation request. Keep only this genuinely
|
|
957
|
+
// unframed race session-ambiguous so a concurrent action can surface
|
|
958
|
+
// uncertainty without guessing which tab created it.
|
|
959
|
+
let frame;
|
|
960
|
+
try {
|
|
961
|
+
frame = request.frame();
|
|
962
|
+
}
|
|
963
|
+
catch {
|
|
964
|
+
this.recordRequestPolicyDenial(error, null);
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
// A denied subframe navigation is real policy enforcement, but it is not
|
|
968
|
+
// the result of the top-level action. Recording it against the tab would
|
|
969
|
+
// falsely turn a successful click into a failed action.
|
|
970
|
+
try {
|
|
971
|
+
if (frame.parentFrame() !== null)
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
catch {
|
|
975
|
+
// A runtime that cannot classify a returned frame has crossed the same
|
|
976
|
+
// attribution boundary as an unregistered popup.
|
|
977
|
+
this.recordRequestPolicyDenial(error, null);
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
try {
|
|
981
|
+
this.refreshPages();
|
|
982
|
+
}
|
|
983
|
+
catch {
|
|
984
|
+
// The policy decision must still be surfaced and the route aborted even
|
|
985
|
+
// if a custom runtime cannot enumerate/register the new page in time.
|
|
986
|
+
this.recordRequestPolicyDenial(error, null);
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
for (const state of this.states.values()) {
|
|
990
|
+
try {
|
|
991
|
+
if (state.page.mainFrame() === frame) {
|
|
992
|
+
this.recordRequestPolicyDenial(error, state.id);
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
catch {
|
|
997
|
+
// A custom runtime may not expose a stable frame identity.
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
// A navigation request can race page registration (notably for popups).
|
|
1001
|
+
// Preserve the policy denial at session scope and report an uncertain
|
|
1002
|
+
// action outcome rather than inventing an attribution.
|
|
1003
|
+
this.recordRequestPolicyDenial(error, null);
|
|
1004
|
+
}
|
|
1005
|
+
recordRequestPolicyDenial(error, tabId) {
|
|
1006
|
+
this.requestPolicyDenialSequence += 1;
|
|
1007
|
+
this.requestPolicyDenials.set(tabId, {
|
|
1008
|
+
sequence: this.requestPolicyDenialSequence,
|
|
1009
|
+
error: tabId === null
|
|
1010
|
+
? new BrowserError("action_failed", "A navigation request was policy-blocked while an action was pending, but its tab could not be attributed; the current action outcome is uncertain.", { cause: error })
|
|
1011
|
+
: error,
|
|
1012
|
+
tabId,
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
requestPolicyDenialAfter(sequence, tabId) {
|
|
1016
|
+
const scoped = this.requestPolicyDenials.get(tabId);
|
|
1017
|
+
const ambiguous = this.requestPolicyDenials.get(null);
|
|
1018
|
+
let latest = null;
|
|
1019
|
+
for (const denial of [scoped, ambiguous]) {
|
|
1020
|
+
if (denial
|
|
1021
|
+
&& denial.sequence > sequence
|
|
1022
|
+
&& (!latest || denial.sequence > latest.sequence)) {
|
|
1023
|
+
latest = denial;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
return latest?.error ?? null;
|
|
749
1027
|
}
|
|
750
1028
|
async newTab(url) {
|
|
751
1029
|
const destination = url ? await this.policy.assertAllowed(url) : null;
|
|
1030
|
+
this.assertOpen();
|
|
752
1031
|
const page = await this.context.newPage();
|
|
1032
|
+
this.assertOpen();
|
|
753
1033
|
const state = this.registerPage(page);
|
|
754
1034
|
if (destination) {
|
|
1035
|
+
const denialSequence = this.requestPolicyDenialSequence;
|
|
755
1036
|
try {
|
|
756
1037
|
this.clearMainDocumentResponse(state);
|
|
757
1038
|
const response = await page.goto(destination.href, {
|
|
@@ -759,9 +1040,15 @@ export class AgentBrowser {
|
|
|
759
1040
|
timeout: this.options.navigationTimeoutMs,
|
|
760
1041
|
});
|
|
761
1042
|
this.queueMainDocumentResponse(state, response);
|
|
1043
|
+
const denial = this.requestPolicyDenialAfter(denialSequence, state.id);
|
|
1044
|
+
if (denial)
|
|
1045
|
+
throw denial;
|
|
762
1046
|
}
|
|
763
1047
|
catch (error) {
|
|
764
1048
|
this.invalidate(state);
|
|
1049
|
+
const denial = this.requestPolicyDenialAfter(denialSequence, state.id);
|
|
1050
|
+
if (denial)
|
|
1051
|
+
throw denial;
|
|
765
1052
|
throw asBrowserError(error, "action_failed", "New-tab navigation was attempted once and did not complete.");
|
|
766
1053
|
}
|
|
767
1054
|
this.invalidate(state);
|
|
@@ -780,6 +1067,7 @@ export class AgentBrowser {
|
|
|
780
1067
|
}
|
|
781
1068
|
this.states.delete(state.id);
|
|
782
1069
|
this.pageStates.delete(state.page);
|
|
1070
|
+
this.requestPolicyDenials.delete(state.id);
|
|
783
1071
|
this.refreshPages();
|
|
784
1072
|
return {
|
|
785
1073
|
ok: true,
|
|
@@ -817,6 +1105,9 @@ export class AgentBrowser {
|
|
|
817
1105
|
}
|
|
818
1106
|
}
|
|
819
1107
|
withLock(operation) {
|
|
1108
|
+
if (this.closed) {
|
|
1109
|
+
return Promise.reject(new BrowserError("browser_closed", "Browser session is closed."));
|
|
1110
|
+
}
|
|
820
1111
|
const previous = this.operationTail;
|
|
821
1112
|
let release;
|
|
822
1113
|
this.operationTail = new Promise((resolveTurn) => {
|
|
@@ -919,16 +1210,31 @@ function authorityContainsUserinfo(reference) {
|
|
|
919
1210
|
const authority = authorityEnd < 0 ? remainder : remainder.slice(0, authorityEnd);
|
|
920
1211
|
return authority.includes("@");
|
|
921
1212
|
}
|
|
1213
|
+
function captureBrowserAction(action) {
|
|
1214
|
+
const captured = {
|
|
1215
|
+
...action,
|
|
1216
|
+
};
|
|
1217
|
+
if (Array.isArray(captured.values)) {
|
|
1218
|
+
captured.values = Object.freeze([...captured.values]);
|
|
1219
|
+
}
|
|
1220
|
+
return Object.freeze(captured);
|
|
1221
|
+
}
|
|
922
1222
|
async function loadDefaultRuntime() {
|
|
923
1223
|
const playwright = await import("playwright-core");
|
|
924
1224
|
return playwright.chromium;
|
|
925
1225
|
}
|
|
1226
|
+
async function closeRuntime(browser, context) {
|
|
1227
|
+
// An ephemeral session owns the Browser process, and Browser.close() closes
|
|
1228
|
+
// its contexts while also terminating a child that a stuck Context.close()
|
|
1229
|
+
// could otherwise orphan. A persistent launch returns only its context.
|
|
1230
|
+
if (browser) {
|
|
1231
|
+
await browser.close();
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
await context?.close();
|
|
1235
|
+
}
|
|
926
1236
|
function normalizeOptions(options) {
|
|
927
|
-
for (const [name, value] of [
|
|
928
|
-
["headless", options.headless],
|
|
929
|
-
["allowPublicWeb", options.allowPublicWeb],
|
|
930
|
-
["allowLocalNetwork", options.allowLocalNetwork],
|
|
931
|
-
]) {
|
|
1237
|
+
for (const [name, value] of [["headless", options.headless]]) {
|
|
932
1238
|
if (value !== undefined && typeof value !== "boolean") {
|
|
933
1239
|
throw new BrowserError("invalid_options", `${name} must be a boolean.`);
|
|
934
1240
|
}
|
|
@@ -995,10 +1301,19 @@ function normalizeOptions(options) {
|
|
|
995
1301
|
if (profile.mode === "persistent") {
|
|
996
1302
|
validateDedicatedProfileDirectory(profile.directory, outputDir);
|
|
997
1303
|
}
|
|
1304
|
+
const capabilities = resolveBrowserCapabilities({
|
|
1305
|
+
...(options.authority ? { authority: options.authority } : {}),
|
|
1306
|
+
...(options.allowPublicWeb !== undefined
|
|
1307
|
+
? { allowPublicWeb: options.allowPublicWeb }
|
|
1308
|
+
: {}),
|
|
1309
|
+
...(options.allowLocalNetwork !== undefined
|
|
1310
|
+
? { allowLocalNetwork: options.allowLocalNetwork }
|
|
1311
|
+
: {}),
|
|
1312
|
+
profileMode: profile.mode,
|
|
1313
|
+
});
|
|
998
1314
|
return {
|
|
999
1315
|
headless: options.headless ?? true,
|
|
1000
|
-
|
|
1001
|
-
allowLocalNetwork: options.allowLocalNetwork ?? false,
|
|
1316
|
+
capabilities,
|
|
1002
1317
|
profile,
|
|
1003
1318
|
...(channel ? { channel } : {}),
|
|
1004
1319
|
...(executablePath ? { executablePath } : {}),
|