browser-use 0.6.0 → 0.7.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/README.md +29 -18
- package/dist/actor/element.js +24 -3
- package/dist/actor/mouse.js +21 -3
- package/dist/actor/page.js +33 -11
- package/dist/agent/gif.js +28 -3
- package/dist/agent/message-manager/service.js +2 -22
- package/dist/agent/message-manager/utils.js +15 -2
- package/dist/agent/message-manager/views.d.ts +7 -7
- package/dist/agent/message-manager/views.js +1 -0
- package/dist/agent/prompts.d.ts +3 -0
- package/dist/agent/prompts.js +22 -12
- package/dist/agent/service.d.ts +9 -1
- package/dist/agent/service.js +215 -81
- package/dist/agent/system_prompt.md +12 -11
- package/dist/agent/system_prompt_anthropic_flash.md +6 -5
- package/dist/agent/system_prompt_no_thinking.md +12 -11
- package/dist/agent/views.d.ts +2 -0
- package/dist/agent/views.js +48 -36
- package/dist/browser/extensions.js +20 -10
- package/dist/browser/profile.d.ts +4 -0
- package/dist/browser/profile.js +107 -4
- package/dist/browser/session.d.ts +28 -1
- package/dist/browser/session.js +1436 -528
- package/dist/browser/watchdogs/default-action-watchdog.js +32 -3
- package/dist/browser/watchdogs/downloads-watchdog.d.ts +4 -0
- package/dist/browser/watchdogs/downloads-watchdog.js +105 -9
- package/dist/browser/watchdogs/har-recording-watchdog.d.ts +1 -0
- package/dist/browser/watchdogs/har-recording-watchdog.js +54 -2
- package/dist/browser/watchdogs/permissions-watchdog.d.ts +5 -0
- package/dist/browser/watchdogs/permissions-watchdog.js +106 -3
- package/dist/browser/watchdogs/recording-watchdog.d.ts +2 -0
- package/dist/browser/watchdogs/recording-watchdog.js +54 -2
- package/dist/browser/watchdogs/security-watchdog.d.ts +1 -0
- package/dist/browser/watchdogs/security-watchdog.js +47 -7
- package/dist/browser/watchdogs/storage-state-watchdog.d.ts +6 -0
- package/dist/browser/watchdogs/storage-state-watchdog.js +206 -14
- package/dist/cli.d.ts +13 -2
- package/dist/cli.js +188 -8
- package/dist/code-use/namespace.js +52 -7
- package/dist/code-use/notebook-export.js +18 -2
- package/dist/code-use/service.js +1 -0
- package/dist/config.js +27 -5
- package/dist/controller/action-timeout.d.ts +9 -0
- package/dist/controller/action-timeout.js +95 -0
- package/dist/controller/registry/service.d.ts +1 -0
- package/dist/controller/registry/service.js +28 -1
- package/dist/controller/registry/views.d.ts +2 -0
- package/dist/controller/registry/views.js +44 -17
- package/dist/controller/service.d.ts +2 -1
- package/dist/controller/service.js +494 -329
- package/dist/filesystem/file-system.js +38 -8
- package/dist/integrations/gmail/service.js +30 -6
- package/dist/llm/browser-use/chat.js +2 -2
- package/dist/llm/codex/auth.d.ts +118 -0
- package/dist/llm/codex/auth.js +599 -0
- package/dist/llm/codex/chat.d.ts +70 -0
- package/dist/llm/codex/chat.js +392 -0
- package/dist/llm/codex/index.d.ts +2 -0
- package/dist/llm/codex/index.js +2 -0
- package/dist/llm/google/chat.js +18 -1
- package/dist/logging-config.js +22 -11
- package/dist/mcp/client.d.ts +1 -0
- package/dist/mcp/client.js +12 -10
- package/dist/mcp/redaction.d.ts +3 -0
- package/dist/mcp/redaction.js +132 -0
- package/dist/mcp/server.d.ts +2 -0
- package/dist/mcp/server.js +64 -22
- package/dist/observability.js +1 -1
- package/dist/screenshots/service.js +25 -2
- package/dist/skill-cli/direct.d.ts +4 -1
- package/dist/skill-cli/direct.js +260 -64
- package/dist/skill-cli/server.d.ts +1 -0
- package/dist/skill-cli/server.js +115 -25
- package/dist/skill-cli/tunnel.d.ts +1 -0
- package/dist/skill-cli/tunnel.js +16 -4
- package/dist/sync/auth.js +22 -9
- package/dist/telemetry/service.js +21 -2
- package/dist/telemetry/views.js +31 -8
- package/dist/tokens/custom-pricing.js +2 -2
- package/dist/tokens/openrouter-pricing.d.ts +11 -0
- package/dist/tokens/openrouter-pricing.js +102 -0
- package/dist/tokens/service.js +20 -16
- package/dist/utils.d.ts +3 -1
- package/dist/utils.js +4 -2
- package/package.json +75 -33
|
@@ -2,7 +2,26 @@ import fs from 'node:fs';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { ClickCoordinateEvent, ClickElementEvent, CloseTabEvent, FileDownloadedEvent, GetDropdownOptionsEvent, GoBackEvent, GoForwardEvent, NavigateToUrlEvent, RefreshEvent, ScrollEvent, ScrollToTextEvent, SelectDropdownOptionEvent, SendKeysEvent, SwitchTabEvent, TypeTextEvent, UploadFileEvent, WaitEvent, } from '../events.js';
|
|
5
|
+
import { URLNotAllowedError } from '../views.js';
|
|
5
6
|
import { BaseWatchdog } from './base.js';
|
|
7
|
+
const chmodPrivatePath = (targetPath, mode) => {
|
|
8
|
+
if (process.platform === 'win32') {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
fs.chmodSync(targetPath, mode);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
/* best effort */
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
const ensurePrivateDirectoryIfCreated = (dirPath) => {
|
|
19
|
+
const existed = fs.existsSync(dirPath);
|
|
20
|
+
fs.mkdirSync(dirPath, { recursive: true, mode: 0o700 });
|
|
21
|
+
if (!existed) {
|
|
22
|
+
chmodPrivatePath(dirPath, 0o700);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
6
25
|
export class DefaultActionWatchdog extends BaseWatchdog {
|
|
7
26
|
static LISTENS_TO = [
|
|
8
27
|
NavigateToUrlEvent,
|
|
@@ -158,26 +177,33 @@ export class DefaultActionWatchdog extends BaseWatchdog {
|
|
|
158
177
|
return null;
|
|
159
178
|
}
|
|
160
179
|
try {
|
|
180
|
+
await this.browser_session.validate_page_after_action(page);
|
|
161
181
|
const cdpSession = await this.browser_session.get_or_create_cdp_session(page);
|
|
182
|
+
await this.browser_session.validate_page_after_action(page);
|
|
162
183
|
const result = await cdpSession.send?.('Page.printToPDF', {
|
|
163
184
|
printBackground: true,
|
|
164
185
|
preferCSSPageSize: true,
|
|
165
186
|
});
|
|
187
|
+
await this.browser_session.validate_page_after_action(page);
|
|
166
188
|
const pdfBase64 = result && typeof result.data === 'string' ? result.data : null;
|
|
167
189
|
if (!pdfBase64) {
|
|
168
190
|
return null;
|
|
169
191
|
}
|
|
170
192
|
const downloadsPath = this.browser_session.browser_profile.downloads_path || os.tmpdir();
|
|
171
|
-
|
|
193
|
+
ensurePrivateDirectoryIfCreated(downloadsPath);
|
|
194
|
+
await this.browser_session.validate_page_after_action(page);
|
|
172
195
|
const title = typeof page.title === 'function' ? await page.title() : 'document';
|
|
196
|
+
await this.browser_session.validate_page_after_action(page);
|
|
173
197
|
const suggestedName = this._sanitizeFilename(`${title || 'document'}.pdf`);
|
|
174
198
|
const uniqueFilename = await this._getUniqueFilename(downloadsPath, suggestedName);
|
|
175
199
|
const finalPath = path.join(downloadsPath, uniqueFilename);
|
|
176
200
|
const content = Buffer.from(pdfBase64, 'base64');
|
|
177
|
-
fs.writeFileSync(finalPath, content);
|
|
201
|
+
fs.writeFileSync(finalPath, content, { mode: 0o600 });
|
|
202
|
+
chmodPrivatePath(finalPath, 0o600);
|
|
203
|
+
const pageUrl = typeof page.url === 'function' ? page.url() : '';
|
|
178
204
|
await this.event_bus.dispatch(new FileDownloadedEvent({
|
|
179
205
|
guid: null,
|
|
180
|
-
url:
|
|
206
|
+
url: pageUrl,
|
|
181
207
|
path: finalPath,
|
|
182
208
|
file_name: uniqueFilename,
|
|
183
209
|
file_size: content.length,
|
|
@@ -188,6 +214,9 @@ export class DefaultActionWatchdog extends BaseWatchdog {
|
|
|
188
214
|
return finalPath;
|
|
189
215
|
}
|
|
190
216
|
catch (error) {
|
|
217
|
+
if (error instanceof URLNotAllowedError) {
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
191
220
|
this.browser_session.logger.debug(`[DefaultActionWatchdog] Print-to-PDF failed: ${error.message}`);
|
|
192
221
|
return null;
|
|
193
222
|
}
|
|
@@ -66,12 +66,16 @@ export declare class DownloadsWatchdog extends BaseWatchdog {
|
|
|
66
66
|
private _normalizeCallbackRegistration;
|
|
67
67
|
private _startCdpDownloadMonitoring;
|
|
68
68
|
private _stopCdpDownloadMonitoring;
|
|
69
|
+
private _getUrlDenialReason;
|
|
69
70
|
private _handleNetworkResponse;
|
|
70
71
|
private _handleNetworkLoadingFinished;
|
|
71
72
|
private _normalizeHeaders;
|
|
72
73
|
private _resolveSuggestedFilename;
|
|
74
|
+
private _decodeFilenameCandidate;
|
|
73
75
|
private _sanitizeFilename;
|
|
74
76
|
private _inferFileType;
|
|
75
77
|
private _getUniqueFilename;
|
|
78
|
+
private _isPathContained;
|
|
79
|
+
private _realPathForMissingPath;
|
|
76
80
|
}
|
|
77
81
|
export {};
|
|
@@ -2,6 +2,24 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { BrowserConnectedEvent, BrowserLaunchEvent, BrowserStateRequestEvent, BrowserStoppedEvent, DownloadProgressEvent, DownloadStartedEvent, FileDownloadedEvent, NavigationCompleteEvent, TabClosedEvent, TabCreatedEvent, } from '../events.js';
|
|
4
4
|
import { BaseWatchdog } from './base.js';
|
|
5
|
+
const chmodPrivatePath = (targetPath, mode) => {
|
|
6
|
+
if (process.platform === 'win32') {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
try {
|
|
10
|
+
fs.chmodSync(targetPath, mode);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
/* best effort */
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
const ensurePrivateDirectoryIfCreated = (dirPath) => {
|
|
17
|
+
const existed = fs.existsSync(dirPath);
|
|
18
|
+
fs.mkdirSync(dirPath, { recursive: true, mode: 0o700 });
|
|
19
|
+
if (!existed) {
|
|
20
|
+
chmodPrivatePath(dirPath, 0o700);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
5
23
|
export class DownloadsWatchdog extends BaseWatchdog {
|
|
6
24
|
static LISTENS_TO = [
|
|
7
25
|
BrowserConnectedEvent,
|
|
@@ -36,7 +54,7 @@ export class DownloadsWatchdog extends BaseWatchdog {
|
|
|
36
54
|
if (!downloadsPath) {
|
|
37
55
|
return;
|
|
38
56
|
}
|
|
39
|
-
|
|
57
|
+
ensurePrivateDirectoryIfCreated(downloadsPath);
|
|
40
58
|
}
|
|
41
59
|
async on_BrowserStateRequestEvent(event) {
|
|
42
60
|
const activeTab = this.browser_session.active_tab;
|
|
@@ -252,6 +270,26 @@ export class DownloadsWatchdog extends BaseWatchdog {
|
|
|
252
270
|
this._cdpSession = null;
|
|
253
271
|
}
|
|
254
272
|
}
|
|
273
|
+
_getUrlDenialReason(url) {
|
|
274
|
+
const session = this.browser_session;
|
|
275
|
+
if (typeof session._get_url_access_denial_reason === 'function') {
|
|
276
|
+
try {
|
|
277
|
+
return session._get_url_access_denial_reason(url);
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
return 'blocked';
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (typeof session._is_url_allowed === 'function') {
|
|
284
|
+
try {
|
|
285
|
+
return session._is_url_allowed(url) ? null : 'blocked';
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
return 'blocked';
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
255
293
|
async _handleNetworkResponse(payload) {
|
|
256
294
|
const requestId = String(payload?.requestId ?? '');
|
|
257
295
|
if (!requestId) {
|
|
@@ -273,6 +311,12 @@ export class DownloadsWatchdog extends BaseWatchdog {
|
|
|
273
311
|
if (!isPdf && !isAttachment && !isBinary) {
|
|
274
312
|
return;
|
|
275
313
|
}
|
|
314
|
+
const denialReason = this._getUrlDenialReason(url);
|
|
315
|
+
if (denialReason) {
|
|
316
|
+
this._detectedDownloadUrls.add(url);
|
|
317
|
+
this.browser_session.logger.warning(`[DownloadsWatchdog] Blocked downloadable network response by domain policy: ${denialReason}`);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
276
320
|
this._detectedDownloadUrls.add(url);
|
|
277
321
|
const suggestedFilename = this._resolveSuggestedFilename(contentDisposition, url);
|
|
278
322
|
const guid = `cdp-${requestId}`;
|
|
@@ -328,13 +372,18 @@ export class DownloadsWatchdog extends BaseWatchdog {
|
|
|
328
372
|
if (!body) {
|
|
329
373
|
return;
|
|
330
374
|
}
|
|
331
|
-
|
|
375
|
+
ensurePrivateDirectoryIfCreated(downloadsPath);
|
|
332
376
|
const uniqueFilename = await this._getUniqueFilename(downloadsPath, metadata.suggested_filename);
|
|
333
377
|
const filePath = path.join(downloadsPath, uniqueFilename);
|
|
378
|
+
if (!this._isPathContained(filePath, downloadsPath)) {
|
|
379
|
+
this.browser_session.logger.debug(`[DownloadsWatchdog] Refusing to write download outside downloads_path: ${filePath}`);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
334
382
|
const content = responseBody?.base64Encoded
|
|
335
383
|
? Buffer.from(body, 'base64')
|
|
336
384
|
: Buffer.from(body, 'utf8');
|
|
337
|
-
fs.writeFileSync(filePath, content);
|
|
385
|
+
fs.writeFileSync(filePath, content, { mode: 0o600 });
|
|
386
|
+
chmodPrivatePath(filePath, 0o600);
|
|
338
387
|
await this.event_bus.dispatch(new FileDownloadedEvent({
|
|
339
388
|
guid: metadata.guid,
|
|
340
389
|
url: metadata.url,
|
|
@@ -362,7 +411,7 @@ export class DownloadsWatchdog extends BaseWatchdog {
|
|
|
362
411
|
_resolveSuggestedFilename(contentDisposition, url) {
|
|
363
412
|
const filenameMatch = /filename\*=UTF-8''([^;]+)|filename="?([^";]+)"?/i.exec(contentDisposition);
|
|
364
413
|
const fromHeader = filenameMatch?.[1] || filenameMatch?.[2] || '';
|
|
365
|
-
const candidate =
|
|
414
|
+
const candidate = this._decodeFilenameCandidate(fromHeader).trim();
|
|
366
415
|
if (candidate) {
|
|
367
416
|
return this._sanitizeFilename(candidate);
|
|
368
417
|
}
|
|
@@ -378,9 +427,29 @@ export class DownloadsWatchdog extends BaseWatchdog {
|
|
|
378
427
|
}
|
|
379
428
|
return 'download';
|
|
380
429
|
}
|
|
430
|
+
_decodeFilenameCandidate(filename) {
|
|
431
|
+
if (!filename) {
|
|
432
|
+
return '';
|
|
433
|
+
}
|
|
434
|
+
try {
|
|
435
|
+
return decodeURIComponent(filename);
|
|
436
|
+
}
|
|
437
|
+
catch {
|
|
438
|
+
return filename;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
381
441
|
_sanitizeFilename(filename) {
|
|
382
|
-
const
|
|
383
|
-
.replace(/
|
|
442
|
+
const basename = filename
|
|
443
|
+
.replace(/\0/g, '')
|
|
444
|
+
.replace(/\\/g, '/')
|
|
445
|
+
.split('/')
|
|
446
|
+
.pop()
|
|
447
|
+
?.trim();
|
|
448
|
+
if (!basename || basename === '.' || basename === '..') {
|
|
449
|
+
return 'download';
|
|
450
|
+
}
|
|
451
|
+
const sanitized = basename
|
|
452
|
+
.replace(/[:*?"<>|]+/g, '_')
|
|
384
453
|
.replace(/\s+/g, ' ')
|
|
385
454
|
.trim();
|
|
386
455
|
return sanitized || 'download';
|
|
@@ -396,9 +465,10 @@ export class DownloadsWatchdog extends BaseWatchdog {
|
|
|
396
465
|
return null;
|
|
397
466
|
}
|
|
398
467
|
async _getUniqueFilename(directory, filename) {
|
|
399
|
-
const
|
|
400
|
-
const
|
|
401
|
-
|
|
468
|
+
const safeFilename = this._sanitizeFilename(filename);
|
|
469
|
+
const ext = path.extname(safeFilename);
|
|
470
|
+
const basename = ext ? safeFilename.slice(0, -ext.length) : safeFilename;
|
|
471
|
+
let candidate = safeFilename || 'download';
|
|
402
472
|
let counter = 1;
|
|
403
473
|
while (fs.existsSync(path.join(directory, candidate))) {
|
|
404
474
|
candidate = `${basename || 'download'}_${counter}${ext}`;
|
|
@@ -406,4 +476,30 @@ export class DownloadsWatchdog extends BaseWatchdog {
|
|
|
406
476
|
}
|
|
407
477
|
return candidate;
|
|
408
478
|
}
|
|
479
|
+
_isPathContained(filePath, directory) {
|
|
480
|
+
const realDirectory = this._realPathForMissingPath(directory);
|
|
481
|
+
const realFilePath = this._realPathForMissingPath(filePath);
|
|
482
|
+
const relative = path.relative(realDirectory, realFilePath);
|
|
483
|
+
return (relative === '' ||
|
|
484
|
+
(relative !== '' &&
|
|
485
|
+
!relative.startsWith('..') &&
|
|
486
|
+
!path.isAbsolute(relative)));
|
|
487
|
+
}
|
|
488
|
+
_realPathForMissingPath(inputPath) {
|
|
489
|
+
const resolvedPath = path.resolve(inputPath);
|
|
490
|
+
if (fs.existsSync(resolvedPath)) {
|
|
491
|
+
return fs.realpathSync.native(resolvedPath);
|
|
492
|
+
}
|
|
493
|
+
const missingParts = [];
|
|
494
|
+
let existingParent = resolvedPath;
|
|
495
|
+
while (!fs.existsSync(existingParent)) {
|
|
496
|
+
const parent = path.dirname(existingParent);
|
|
497
|
+
if (parent === existingParent) {
|
|
498
|
+
return resolvedPath;
|
|
499
|
+
}
|
|
500
|
+
missingParts.unshift(path.basename(existingParent));
|
|
501
|
+
existingParent = parent;
|
|
502
|
+
}
|
|
503
|
+
return path.join(fs.realpathSync.native(existingParent), ...missingParts);
|
|
504
|
+
}
|
|
409
505
|
}
|
|
@@ -32,6 +32,31 @@ const normalizeHeaders = (input) => {
|
|
|
32
32
|
}
|
|
33
33
|
return {};
|
|
34
34
|
};
|
|
35
|
+
const chmodPrivatePath = (targetPath, mode) => {
|
|
36
|
+
if (process.platform === 'win32') {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
fs.chmodSync(targetPath, mode);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
/* best effort */
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const ensurePrivateDirectoryIfCreated = (dirPath) => {
|
|
47
|
+
const existed = fs.existsSync(dirPath);
|
|
48
|
+
fs.mkdirSync(dirPath, { recursive: true, mode: 0o700 });
|
|
49
|
+
if (!existed) {
|
|
50
|
+
chmodPrivatePath(dirPath, 0o700);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
const writePrivateFile = (filePath, contents) => {
|
|
54
|
+
fs.writeFileSync(filePath, contents, {
|
|
55
|
+
encoding: 'utf-8',
|
|
56
|
+
mode: 0o600,
|
|
57
|
+
});
|
|
58
|
+
chmodPrivatePath(filePath, 0o600);
|
|
59
|
+
};
|
|
35
60
|
export class HarRecordingWatchdog extends BaseWatchdog {
|
|
36
61
|
static LISTENS_TO = [
|
|
37
62
|
BrowserStartEvent,
|
|
@@ -74,6 +99,7 @@ export class HarRecordingWatchdog extends BaseWatchdog {
|
|
|
74
99
|
return;
|
|
75
100
|
}
|
|
76
101
|
try {
|
|
102
|
+
chmodPrivatePath(resolvedPath, 0o600);
|
|
77
103
|
const stat = fs.statSync(resolvedPath);
|
|
78
104
|
if (stat.size === 0) {
|
|
79
105
|
await this.event_bus.dispatch(new BrowserErrorEvent({
|
|
@@ -113,7 +139,7 @@ export class HarRecordingWatchdog extends BaseWatchdog {
|
|
|
113
139
|
if (!resolvedPath) {
|
|
114
140
|
return null;
|
|
115
141
|
}
|
|
116
|
-
|
|
142
|
+
ensurePrivateDirectoryIfCreated(path.dirname(resolvedPath));
|
|
117
143
|
this.browser_session.browser_profile.config.record_har_path = resolvedPath;
|
|
118
144
|
return resolvedPath;
|
|
119
145
|
}
|
|
@@ -132,6 +158,11 @@ export class HarRecordingWatchdog extends BaseWatchdog {
|
|
|
132
158
|
if (!requestId || !url.toLowerCase().startsWith('https://')) {
|
|
133
159
|
return;
|
|
134
160
|
}
|
|
161
|
+
const denialReason = this._getUrlDenialReason(url);
|
|
162
|
+
if (denialReason) {
|
|
163
|
+
this.browser_session.logger.debug(`[HarRecordingWatchdog] Skipping HAR entry blocked by domain policy: ${denialReason}`);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
135
166
|
const tsRequest = typeof payload?.timestamp === 'number' ? payload.timestamp : null;
|
|
136
167
|
this._entries.set(requestId, {
|
|
137
168
|
request_id: requestId,
|
|
@@ -292,8 +323,29 @@ export class HarRecordingWatchdog extends BaseWatchdog {
|
|
|
292
323
|
},
|
|
293
324
|
};
|
|
294
325
|
const tempPath = `${resolvedPath}.tmp`;
|
|
295
|
-
|
|
326
|
+
writePrivateFile(tempPath, JSON.stringify(harObject, null, 2));
|
|
296
327
|
fs.renameSync(tempPath, resolvedPath);
|
|
328
|
+
chmodPrivatePath(resolvedPath, 0o600);
|
|
329
|
+
}
|
|
330
|
+
_getUrlDenialReason(url) {
|
|
331
|
+
const session = this.browser_session;
|
|
332
|
+
if (typeof session._get_url_access_denial_reason === 'function') {
|
|
333
|
+
try {
|
|
334
|
+
return session._get_url_access_denial_reason(url);
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
return 'blocked';
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (typeof session._is_url_allowed === 'function') {
|
|
341
|
+
try {
|
|
342
|
+
return session._is_url_allowed(url) ? null : 'blocked';
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
return 'blocked';
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return null;
|
|
297
349
|
}
|
|
298
350
|
async _teardownCapture() {
|
|
299
351
|
if (!this._cdpSession) {
|
|
@@ -4,5 +4,10 @@ export declare class PermissionsWatchdog extends BaseWatchdog {
|
|
|
4
4
|
static LISTENS_TO: (typeof BrowserConnectedEvent)[];
|
|
5
5
|
static EMITS: (typeof BrowserErrorEvent)[];
|
|
6
6
|
on_BrowserConnectedEvent(): Promise<void>;
|
|
7
|
+
private _grantScopedPermissions;
|
|
8
|
+
private _dispatchPermissionError;
|
|
9
|
+
private _getScopedPermissionOrigins;
|
|
10
|
+
private _domainCollectionToStrings;
|
|
11
|
+
private _originsForAllowedPattern;
|
|
7
12
|
private _grantPermissionsViaCdp;
|
|
8
13
|
}
|
|
@@ -8,6 +8,11 @@ export class PermissionsWatchdog extends BaseWatchdog {
|
|
|
8
8
|
if (!Array.isArray(permissions) || permissions.length === 0) {
|
|
9
9
|
return;
|
|
10
10
|
}
|
|
11
|
+
const scopedOrigins = this._getScopedPermissionOrigins();
|
|
12
|
+
if (scopedOrigins) {
|
|
13
|
+
await this._grantScopedPermissions(permissions, scopedOrigins);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
11
16
|
let cdpError = null;
|
|
12
17
|
try {
|
|
13
18
|
const grantedWithCdp = await this._grantPermissionsViaCdp(permissions);
|
|
@@ -49,16 +54,114 @@ export class PermissionsWatchdog extends BaseWatchdog {
|
|
|
49
54
|
}));
|
|
50
55
|
}
|
|
51
56
|
}
|
|
52
|
-
async
|
|
57
|
+
async _grantScopedPermissions(permissions, origins) {
|
|
58
|
+
if (origins.length === 0) {
|
|
59
|
+
this.browser_session.logger.debug('[PermissionsWatchdog] Domain restrictions are active; skipping global permission grants because no concrete allowed origins can be resolved.');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const context = this.browser_session.browser_context;
|
|
63
|
+
for (const origin of origins) {
|
|
64
|
+
let cdpError = null;
|
|
65
|
+
try {
|
|
66
|
+
const grantedWithCdp = await this._grantPermissionsViaCdp(permissions, origin);
|
|
67
|
+
if (grantedWithCdp) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
cdpError = error;
|
|
73
|
+
}
|
|
74
|
+
if (!context?.grantPermissions) {
|
|
75
|
+
if (cdpError) {
|
|
76
|
+
await this._dispatchPermissionError(permissions, 'cdp', cdpError, origin);
|
|
77
|
+
}
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
await context.grantPermissions(permissions, { origin });
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
await this._dispatchPermissionError(permissions, 'playwright', error, origin, cdpError);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async _dispatchPermissionError(permissions, mode, error, origin, cdpError) {
|
|
89
|
+
await this.event_bus.dispatch(new BrowserErrorEvent({
|
|
90
|
+
error_type: 'PermissionsWatchdogError',
|
|
91
|
+
message: error.message ?? 'Failed to grant permissions',
|
|
92
|
+
details: {
|
|
93
|
+
permissions,
|
|
94
|
+
cdp_error: cdpError?.message ?? null,
|
|
95
|
+
mode,
|
|
96
|
+
origin: origin ?? null,
|
|
97
|
+
},
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
_getScopedPermissionOrigins() {
|
|
101
|
+
const session = this.browser_session;
|
|
102
|
+
const hasRestrictions = typeof session._has_url_access_restrictions === 'function'
|
|
103
|
+
? session._has_url_access_restrictions()
|
|
104
|
+
: false;
|
|
105
|
+
if (!hasRestrictions) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
const allowedDomains = this._domainCollectionToStrings(this.browser_session.browser_profile.allowed_domains);
|
|
109
|
+
const origins = new Set();
|
|
110
|
+
for (const pattern of allowedDomains) {
|
|
111
|
+
for (const origin of this._originsForAllowedPattern(pattern)) {
|
|
112
|
+
const denialReason = typeof session._get_url_access_denial_reason === 'function'
|
|
113
|
+
? session._get_url_access_denial_reason(origin)
|
|
114
|
+
: null;
|
|
115
|
+
if (!denialReason) {
|
|
116
|
+
origins.add(origin);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return [...origins];
|
|
121
|
+
}
|
|
122
|
+
_domainCollectionToStrings(value) {
|
|
123
|
+
if (value instanceof Set) {
|
|
124
|
+
return [...value].filter((item) => typeof item === 'string');
|
|
125
|
+
}
|
|
126
|
+
if (Array.isArray(value)) {
|
|
127
|
+
return value.filter((item) => typeof item === 'string');
|
|
128
|
+
}
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
_originsForAllowedPattern(pattern) {
|
|
132
|
+
const trimmed = pattern.trim();
|
|
133
|
+
if (!trimmed || trimmed.includes('*')) {
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
if (trimmed.includes('://')) {
|
|
138
|
+
const parsed = new URL(trimmed);
|
|
139
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
return [parsed.origin];
|
|
143
|
+
}
|
|
144
|
+
const parsed = new URL(`https://${trimmed}`);
|
|
145
|
+
return [parsed.origin];
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async _grantPermissionsViaCdp(permissions, origin) {
|
|
53
152
|
const browser = this.browser_session.browser;
|
|
54
153
|
if (!browser?.newBrowserCDPSession) {
|
|
55
154
|
return false;
|
|
56
155
|
}
|
|
57
156
|
const cdpSession = await browser.newBrowserCDPSession();
|
|
58
157
|
try {
|
|
59
|
-
|
|
158
|
+
const params = {
|
|
60
159
|
permissions,
|
|
61
|
-
}
|
|
160
|
+
};
|
|
161
|
+
if (origin) {
|
|
162
|
+
params.origin = origin;
|
|
163
|
+
}
|
|
164
|
+
await cdpSession.send?.('Browser.grantPermissions', params);
|
|
62
165
|
return true;
|
|
63
166
|
}
|
|
64
167
|
finally {
|
|
@@ -8,12 +8,14 @@ export declare class RecordingWatchdog extends BaseWatchdog {
|
|
|
8
8
|
private _cdpScreencastHandler;
|
|
9
9
|
private _cdpScreencastPath;
|
|
10
10
|
private _cdpScreencastStream;
|
|
11
|
+
private _warnedDomainPolicyRecordingDisabled;
|
|
11
12
|
on_BrowserConnectedEvent(): Promise<void>;
|
|
12
13
|
on_BrowserStopEvent(): Promise<void>;
|
|
13
14
|
on_BrowserStoppedEvent(): Promise<void>;
|
|
14
15
|
on_AgentFocusChangedEvent(event: AgentFocusChangedEvent): Promise<void>;
|
|
15
16
|
on_TabCreatedEvent(): Promise<void>;
|
|
16
17
|
protected onDetached(): void;
|
|
18
|
+
private _recordingDisabledByDomainPolicy;
|
|
17
19
|
private _prepareVideoDirectory;
|
|
18
20
|
private _startTracingIfConfigured;
|
|
19
21
|
private _stopTracingIfStarted;
|
|
@@ -2,6 +2,24 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { AgentFocusChangedEvent, BrowserConnectedEvent, BrowserErrorEvent, BrowserStopEvent, BrowserStoppedEvent, TabCreatedEvent, } from '../events.js';
|
|
4
4
|
import { BaseWatchdog } from './base.js';
|
|
5
|
+
const chmodPrivatePath = (targetPath, mode) => {
|
|
6
|
+
if (process.platform === 'win32') {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
try {
|
|
10
|
+
fs.chmodSync(targetPath, mode);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
/* best effort */
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
const ensurePrivateDirectoryIfCreated = (dirPath) => {
|
|
17
|
+
const existed = fs.existsSync(dirPath);
|
|
18
|
+
fs.mkdirSync(dirPath, { recursive: true, mode: 0o700 });
|
|
19
|
+
if (!existed) {
|
|
20
|
+
chmodPrivatePath(dirPath, 0o700);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
5
23
|
export class RecordingWatchdog extends BaseWatchdog {
|
|
6
24
|
static LISTENS_TO = [
|
|
7
25
|
BrowserConnectedEvent,
|
|
@@ -16,7 +34,11 @@ export class RecordingWatchdog extends BaseWatchdog {
|
|
|
16
34
|
_cdpScreencastHandler = null;
|
|
17
35
|
_cdpScreencastPath = null;
|
|
18
36
|
_cdpScreencastStream = null;
|
|
37
|
+
_warnedDomainPolicyRecordingDisabled = false;
|
|
19
38
|
async on_BrowserConnectedEvent() {
|
|
39
|
+
if (this._recordingDisabledByDomainPolicy()) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
20
42
|
this._prepareVideoDirectory();
|
|
21
43
|
this._attachVideoListenersToKnownPages();
|
|
22
44
|
await this._startCdpScreencastIfConfigured();
|
|
@@ -30,6 +52,9 @@ export class RecordingWatchdog extends BaseWatchdog {
|
|
|
30
52
|
this._detachVideoListeners();
|
|
31
53
|
}
|
|
32
54
|
async on_AgentFocusChangedEvent(event) {
|
|
55
|
+
if (this._recordingDisabledByDomainPolicy()) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
33
58
|
this._attachVideoListenersToKnownPages();
|
|
34
59
|
await this._startCdpScreencastIfConfigured();
|
|
35
60
|
if (!this._traceStarted) {
|
|
@@ -38,6 +63,9 @@ export class RecordingWatchdog extends BaseWatchdog {
|
|
|
38
63
|
this.browser_session.logger.debug(`[RecordingWatchdog] Focus changed to ${event.target_id}; tracing remains active`);
|
|
39
64
|
}
|
|
40
65
|
async on_TabCreatedEvent() {
|
|
66
|
+
if (this._recordingDisabledByDomainPolicy()) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
41
69
|
this._attachVideoListenersToKnownPages();
|
|
42
70
|
await this._startCdpScreencastIfConfigured();
|
|
43
71
|
}
|
|
@@ -45,13 +73,32 @@ export class RecordingWatchdog extends BaseWatchdog {
|
|
|
45
73
|
void this._stopCdpScreencastIfStarted();
|
|
46
74
|
this._detachVideoListeners();
|
|
47
75
|
}
|
|
76
|
+
_recordingDisabledByDomainPolicy() {
|
|
77
|
+
const hasRestrictions = typeof this.browser_session._has_url_access_restrictions ===
|
|
78
|
+
'function'
|
|
79
|
+
? this.browser_session._has_url_access_restrictions()
|
|
80
|
+
: false;
|
|
81
|
+
if (!hasRestrictions) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
const hasRecordingConfigured = Boolean(this.browser_session.browser_profile.traces_dir ||
|
|
85
|
+
this.browser_session.browser_profile.config.record_video_dir);
|
|
86
|
+
if (!hasRecordingConfigured) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
if (!this._warnedDomainPolicyRecordingDisabled) {
|
|
90
|
+
this.browser_session.logger.warning('[RecordingWatchdog] Skipping trace/video recording because domain restrictions are active and recording artifacts cannot be URL-filtered.');
|
|
91
|
+
this._warnedDomainPolicyRecordingDisabled = true;
|
|
92
|
+
}
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
48
95
|
_prepareVideoDirectory() {
|
|
49
96
|
const configuredPath = this.browser_session.browser_profile.config.record_video_dir;
|
|
50
97
|
if (typeof configuredPath !== 'string' || configuredPath.trim() === '') {
|
|
51
98
|
return;
|
|
52
99
|
}
|
|
53
100
|
const resolvedPath = path.resolve(configuredPath);
|
|
54
|
-
|
|
101
|
+
ensurePrivateDirectoryIfCreated(resolvedPath);
|
|
55
102
|
this.browser_session.browser_profile.config.record_video_dir = resolvedPath;
|
|
56
103
|
}
|
|
57
104
|
async _startTracingIfConfigured() {
|
|
@@ -171,7 +218,11 @@ export class RecordingWatchdog extends BaseWatchdog {
|
|
|
171
218
|
}
|
|
172
219
|
const session = (await this.browser_session.get_or_create_cdp_session(page));
|
|
173
220
|
const filePath = path.join(configuredPath, `${Date.now()}-${this.browser_session.id.slice(-6)}.cdp-screencast.ndjson`);
|
|
174
|
-
const stream = fs.createWriteStream(filePath, {
|
|
221
|
+
const stream = fs.createWriteStream(filePath, {
|
|
222
|
+
flags: 'a',
|
|
223
|
+
mode: 0o600,
|
|
224
|
+
});
|
|
225
|
+
chmodPrivatePath(filePath, 0o600);
|
|
175
226
|
const handler = (payload) => {
|
|
176
227
|
const frameData = typeof payload?.data === 'string' ? payload.data : '';
|
|
177
228
|
if (frameData && this._cdpScreencastStream) {
|
|
@@ -239,6 +290,7 @@ export class RecordingWatchdog extends BaseWatchdog {
|
|
|
239
290
|
if (this._cdpScreencastPath &&
|
|
240
291
|
fs.existsSync(this._cdpScreencastPath) &&
|
|
241
292
|
fs.statSync(this._cdpScreencastPath).size > 0) {
|
|
293
|
+
chmodPrivatePath(this._cdpScreencastPath, 0o600);
|
|
242
294
|
this.browser_session.add_downloaded_file(this._cdpScreencastPath);
|
|
243
295
|
}
|
|
244
296
|
this._cdpScreencastSession = null;
|