browser-use 0.6.1 → 0.7.1

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.
Files changed (84) hide show
  1. package/README.md +24 -18
  2. package/dist/actor/element.js +24 -3
  3. package/dist/actor/mouse.js +21 -3
  4. package/dist/actor/page.js +33 -11
  5. package/dist/agent/gif.js +28 -3
  6. package/dist/agent/message-manager/service.js +2 -22
  7. package/dist/agent/message-manager/utils.js +15 -2
  8. package/dist/agent/message-manager/views.d.ts +7 -7
  9. package/dist/agent/message-manager/views.js +1 -0
  10. package/dist/agent/prompts.d.ts +3 -0
  11. package/dist/agent/prompts.js +22 -12
  12. package/dist/agent/service.d.ts +9 -1
  13. package/dist/agent/service.js +204 -79
  14. package/dist/agent/system_prompt.md +12 -11
  15. package/dist/agent/system_prompt_anthropic_flash.md +6 -5
  16. package/dist/agent/system_prompt_no_thinking.md +12 -11
  17. package/dist/agent/views.d.ts +2 -0
  18. package/dist/agent/views.js +48 -36
  19. package/dist/browser/extensions.js +20 -10
  20. package/dist/browser/profile.d.ts +4 -0
  21. package/dist/browser/profile.js +107 -4
  22. package/dist/browser/session.d.ts +28 -1
  23. package/dist/browser/session.js +1436 -528
  24. package/dist/browser/watchdogs/default-action-watchdog.js +32 -3
  25. package/dist/browser/watchdogs/downloads-watchdog.d.ts +4 -0
  26. package/dist/browser/watchdogs/downloads-watchdog.js +105 -9
  27. package/dist/browser/watchdogs/har-recording-watchdog.d.ts +1 -0
  28. package/dist/browser/watchdogs/har-recording-watchdog.js +54 -2
  29. package/dist/browser/watchdogs/permissions-watchdog.d.ts +5 -0
  30. package/dist/browser/watchdogs/permissions-watchdog.js +106 -3
  31. package/dist/browser/watchdogs/recording-watchdog.d.ts +2 -0
  32. package/dist/browser/watchdogs/recording-watchdog.js +54 -2
  33. package/dist/browser/watchdogs/security-watchdog.d.ts +1 -0
  34. package/dist/browser/watchdogs/security-watchdog.js +47 -7
  35. package/dist/browser/watchdogs/storage-state-watchdog.d.ts +6 -0
  36. package/dist/browser/watchdogs/storage-state-watchdog.js +206 -14
  37. package/dist/cli.d.ts +13 -2
  38. package/dist/cli.js +190 -9
  39. package/dist/code-use/namespace.js +52 -7
  40. package/dist/code-use/notebook-export.js +18 -2
  41. package/dist/code-use/service.js +1 -0
  42. package/dist/config.js +26 -4
  43. package/dist/controller/action-timeout.d.ts +9 -0
  44. package/dist/controller/action-timeout.js +95 -0
  45. package/dist/controller/registry/service.d.ts +1 -0
  46. package/dist/controller/registry/service.js +28 -1
  47. package/dist/controller/service.d.ts +2 -1
  48. package/dist/controller/service.js +494 -329
  49. package/dist/entrypoint.d.ts +1 -0
  50. package/dist/entrypoint.js +27 -0
  51. package/dist/filesystem/file-system.js +38 -8
  52. package/dist/integrations/gmail/service.js +30 -6
  53. package/dist/llm/browser-use/chat.js +2 -2
  54. package/dist/llm/codex/auth.d.ts +118 -0
  55. package/dist/llm/codex/auth.js +599 -0
  56. package/dist/llm/codex/chat.d.ts +70 -0
  57. package/dist/llm/codex/chat.js +392 -0
  58. package/dist/llm/codex/index.d.ts +2 -0
  59. package/dist/llm/codex/index.js +2 -0
  60. package/dist/llm/google/chat.js +18 -1
  61. package/dist/logging-config.js +22 -11
  62. package/dist/mcp/client.d.ts +1 -0
  63. package/dist/mcp/client.js +12 -10
  64. package/dist/mcp/redaction.d.ts +3 -0
  65. package/dist/mcp/redaction.js +132 -0
  66. package/dist/mcp/server.d.ts +2 -0
  67. package/dist/mcp/server.js +64 -22
  68. package/dist/screenshots/service.js +25 -2
  69. package/dist/skill-cli/direct.d.ts +4 -1
  70. package/dist/skill-cli/direct.js +263 -66
  71. package/dist/skill-cli/server.d.ts +1 -0
  72. package/dist/skill-cli/server.js +115 -25
  73. package/dist/skill-cli/tunnel.d.ts +1 -0
  74. package/dist/skill-cli/tunnel.js +16 -4
  75. package/dist/sync/auth.js +22 -9
  76. package/dist/telemetry/service.js +21 -2
  77. package/dist/telemetry/views.js +31 -8
  78. package/dist/tokens/custom-pricing.js +2 -2
  79. package/dist/tokens/openrouter-pricing.d.ts +11 -0
  80. package/dist/tokens/openrouter-pricing.js +102 -0
  81. package/dist/tokens/service.js +20 -16
  82. package/dist/utils.d.ts +3 -1
  83. package/dist/utils.js +3 -1
  84. package/package.json +68 -27
@@ -1,4 +1,4 @@
1
- import { promises as fsp } from 'node:fs';
1
+ import fs, { promises as fsp } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { Request, Response } from './protocol.js';
4
4
  import { SessionRegistry } from './sessions.js';
@@ -6,6 +6,22 @@ const normalizeCookieDomain = (value) => String(value ?? '')
6
6
  .trim()
7
7
  .replace(/^\./, '')
8
8
  .toLowerCase();
9
+ const chmodPrivateFile = (filePath) => {
10
+ if (process.platform !== 'win32') {
11
+ fs.chmodSync(filePath, 0o600);
12
+ }
13
+ };
14
+ const writePrivateJsonFile = async (filePath, data) => {
15
+ await fsp.writeFile(filePath, JSON.stringify(data, null, 2), {
16
+ encoding: 'utf8',
17
+ mode: 0o600,
18
+ });
19
+ chmodPrivateFile(filePath);
20
+ };
21
+ const writePrivateBinaryFile = async (filePath, data) => {
22
+ await fsp.writeFile(filePath, data, { mode: 0o600 });
23
+ chmodPrivateFile(filePath);
24
+ };
9
25
  const parseCookieHostname = (url) => {
10
26
  const value = String(url ?? '').trim();
11
27
  if (!value) {
@@ -49,9 +65,7 @@ const cookieMatchesUrl = (cookie, url) => {
49
65
  if (!hostname || !domain) {
50
66
  return false;
51
67
  }
52
- if (!(hostname === domain ||
53
- hostname.endsWith(`.${domain}`) ||
54
- domain.endsWith(`.${hostname}`))) {
68
+ if (!(hostname === domain || hostname.endsWith(`.${domain}`))) {
55
69
  return false;
56
70
  }
57
71
  if (!cookiePathMatches(cookie.path, parsedUrl?.pathname || '/')) {
@@ -84,6 +98,33 @@ const parseCookieExpires = (value) => {
84
98
  const parsed = Number(value);
85
99
  return Number.isFinite(parsed) ? parsed : undefined;
86
100
  };
101
+ const getCookieDenialReason = (session, cookie) => {
102
+ const checker = session?._get_cookie_access_denial_reason;
103
+ if (typeof checker !== 'function') {
104
+ return null;
105
+ }
106
+ return checker.call(session, cookie);
107
+ };
108
+ const filterAllowedCookies = (session, cookies) => cookies.filter((cookie) => !getCookieDenialReason(session, cookie));
109
+ const partitionAllowedCookies = (session, cookies) => {
110
+ const allowedCookies = [];
111
+ const blockedCookies = [];
112
+ for (const cookie of cookies) {
113
+ if (getCookieDenialReason(session, cookie)) {
114
+ blockedCookies.push(cookie);
115
+ }
116
+ else {
117
+ allowedCookies.push(cookie);
118
+ }
119
+ }
120
+ return { allowedCookies, blockedCookies };
121
+ };
122
+ const assertCookieUrlAllowed = (session, url) => {
123
+ const denialReason = getCookieDenialReason(session, { url });
124
+ if (denialReason) {
125
+ throw new Error(`Cookie URL blocked by domain policy: ${denialReason}`);
126
+ }
127
+ };
87
128
  export class SkillCliServer {
88
129
  registry;
89
130
  constructor(options = {}) {
@@ -102,6 +143,16 @@ export class SkillCliServer {
102
143
  }
103
144
  return node;
104
145
  }
146
+ async _run_with_page_validation(browser_session, action) {
147
+ const page = await browser_session.get_current_page?.();
148
+ await browser_session.validate_page_after_action?.(page);
149
+ try {
150
+ return await action();
151
+ }
152
+ finally {
153
+ await browser_session.validate_page_after_action?.(page);
154
+ }
155
+ }
105
156
  async _read_node_data(browser_session, node, kind) {
106
157
  if (!node?.xpath) {
107
158
  throw new Error('DOM element does not include an XPath selector');
@@ -110,7 +161,7 @@ export class SkillCliServer {
110
161
  if (!page?.evaluate) {
111
162
  throw new Error('No active page available');
112
163
  }
113
- return await page.evaluate(({ xpath, dataKind }) => {
164
+ return await this._run_with_page_validation(browser_session, () => page.evaluate(({ xpath, dataKind }) => {
114
165
  const element = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
115
166
  if (!element) {
116
167
  return null;
@@ -143,7 +194,7 @@ export class SkillCliServer {
143
194
  };
144
195
  }
145
196
  return null;
146
- }, { xpath: node.xpath, dataKind: kind });
197
+ }, { xpath: node.xpath, dataKind: kind }));
147
198
  }
148
199
  async _handle_browser_action(action, sessionName, params) {
149
200
  const session = await this.registry.get_or_create_session(sessionName);
@@ -176,7 +227,7 @@ export class SkillCliServer {
176
227
  if (!locator?.hover) {
177
228
  throw new Error('Hover is not available for this element');
178
229
  }
179
- await locator.hover({ timeout: 5000 });
230
+ await this._run_with_page_validation(browser_session, async () => locator.hover({ timeout: 5000 }));
180
231
  return { hovered: Number(params.index) };
181
232
  }
182
233
  if (action === 'dblclick') {
@@ -188,7 +239,7 @@ export class SkillCliServer {
188
239
  if (!locator?.dblclick) {
189
240
  throw new Error('Double-click is not available for this element');
190
241
  }
191
- await locator.dblclick({ timeout: 5000 });
242
+ await this._run_with_page_validation(browser_session, async () => locator.dblclick({ timeout: 5000 }));
192
243
  return { double_clicked: Number(params.index) };
193
244
  }
194
245
  if (action === 'rightclick') {
@@ -200,7 +251,7 @@ export class SkillCliServer {
200
251
  if (!locator?.click) {
201
252
  throw new Error('Right-click is not available for this element');
202
253
  }
203
- await locator.click({ button: 'right', timeout: 5000 });
254
+ await this._run_with_page_validation(browser_session, async () => locator.click({ button: 'right', timeout: 5000 }));
204
255
  return { right_clicked: Number(params.index) };
205
256
  }
206
257
  if (action === 'type') {
@@ -243,7 +294,7 @@ export class SkillCliServer {
243
294
  return { screenshot };
244
295
  }
245
296
  const filePath = path.resolve(file);
246
- await fsp.writeFile(filePath, Buffer.from(screenshot, 'base64'));
297
+ await writePrivateBinaryFile(filePath, Buffer.from(screenshot, 'base64'));
247
298
  return { file: filePath };
248
299
  }
249
300
  if (action === 'wait_selector') {
@@ -265,7 +316,7 @@ export class SkillCliServer {
265
316
  if (!page?.waitForFunction) {
266
317
  throw new Error('No active page available for wait_text');
267
318
  }
268
- await page.waitForFunction((needle) => document.body?.innerText?.includes(needle) ?? false, text, { timeout });
319
+ await this._run_with_page_validation(browser_session, () => page.waitForFunction((needle) => document.body?.innerText?.includes(needle) ?? false, text, { timeout }));
269
320
  return { waited_for: 'text', text, timeout };
270
321
  }
271
322
  if (action === 'scroll') {
@@ -344,10 +395,10 @@ export class SkillCliServer {
344
395
  if (!page?.evaluate) {
345
396
  throw new Error('No active page available for html');
346
397
  }
347
- const html = await page.evaluate((targetSelector) => {
398
+ const html = await this._run_with_page_validation(browser_session, () => page.evaluate((targetSelector) => {
348
399
  const element = document.querySelector(targetSelector);
349
400
  return element ? element.outerHTML : null;
350
- }, selector);
401
+ }, selector));
351
402
  if (typeof html !== 'string' || html.length === 0) {
352
403
  throw new Error(`No element found for selector: ${selector}`);
353
404
  }
@@ -378,7 +429,7 @@ export class SkillCliServer {
378
429
  throw new Error('No active page available for get_title');
379
430
  }
380
431
  return {
381
- title: await page.title(),
432
+ title: await this._run_with_page_validation(browser_session, () => page.title()),
382
433
  };
383
434
  }
384
435
  if (action === 'get_html') {
@@ -407,10 +458,14 @@ export class SkillCliServer {
407
458
  }
408
459
  if (action === 'cookies_get') {
409
460
  const url = typeof params.url === 'string' ? params.url.trim() : '';
461
+ if (url) {
462
+ assertCookieUrlAllowed(browser_session, url);
463
+ }
410
464
  const allCookies = (await browser_session.get_cookies());
465
+ const allowedCookies = filterAllowedCookies(browser_session, allCookies);
411
466
  const cookies = url
412
- ? allCookies.filter((cookie) => cookieMatchesUrl(cookie, url))
413
- : allCookies;
467
+ ? allowedCookies.filter((cookie) => cookieMatchesUrl(cookie, url))
468
+ : allowedCookies;
414
469
  return { cookies, count: cookies.length };
415
470
  }
416
471
  if (action === 'cookies_set') {
@@ -443,6 +498,10 @@ export class SkillCliServer {
443
498
  if (!cookie.url && !cookie.domain) {
444
499
  throw new Error('Provide cookie url/domain or open a page first');
445
500
  }
501
+ const denialReason = getCookieDenialReason(browser_session, cookie);
502
+ if (denialReason) {
503
+ throw new Error(`Cookie target blocked by domain policy: ${denialReason}`);
504
+ }
446
505
  await browser_session.browser_context.addCookies([cookie]);
447
506
  return { set: name };
448
507
  }
@@ -452,15 +511,39 @@ export class SkillCliServer {
452
511
  }
453
512
  const url = typeof params.url === 'string' ? params.url.trim() : '';
454
513
  if (!url) {
514
+ if (typeof browser_session.get_cookies !== 'function') {
515
+ await browser_session.browser_context.clearCookies();
516
+ return { cleared: true };
517
+ }
518
+ const allCookies = (await browser_session.get_cookies({
519
+ include_blocked: true,
520
+ }));
521
+ const { allowedCookies, blockedCookies } = partitionAllowedCookies(browser_session, allCookies);
522
+ if (allowedCookies.length === 0 && blockedCookies.length > 0) {
523
+ return { cleared: true, count: 0 };
524
+ }
525
+ if (blockedCookies.length > 0 &&
526
+ !browser_session.browser_context.addCookies) {
527
+ throw new Error('Browser context does not support preserving blocked cookies');
528
+ }
455
529
  await browser_session.browser_context.clearCookies();
456
- return { cleared: true };
530
+ if (blockedCookies.length > 0) {
531
+ await browser_session.browser_context.addCookies(blockedCookies);
532
+ }
533
+ return { cleared: true, count: allowedCookies.length };
457
534
  }
458
- const allCookies = (await browser_session.get_cookies());
535
+ assertCookieUrlAllowed(browser_session, url);
536
+ const allCookies = (await browser_session.get_cookies({
537
+ include_blocked: true,
538
+ }));
459
539
  const remainingCookies = allCookies.filter((cookie) => !cookieMatchesUrl(cookie, url));
460
540
  const removedCount = allCookies.length - remainingCookies.length;
461
- await browser_session.browser_context.clearCookies();
462
541
  if (remainingCookies.length > 0 &&
463
- browser_session.browser_context.addCookies) {
542
+ !browser_session.browser_context.addCookies) {
543
+ throw new Error('Browser context does not support preserving non-matching cookies');
544
+ }
545
+ await browser_session.browser_context.clearCookies();
546
+ if (remainingCookies.length > 0) {
464
547
  await browser_session.browser_context.addCookies(remainingCookies);
465
548
  }
466
549
  return { cleared: true, url, count: removedCount };
@@ -471,12 +554,16 @@ export class SkillCliServer {
471
554
  throw new Error('Missing file');
472
555
  }
473
556
  const url = typeof params.url === 'string' ? params.url.trim() : '';
557
+ if (url) {
558
+ assertCookieUrlAllowed(browser_session, url);
559
+ }
474
560
  const allCookies = (await browser_session.get_cookies());
561
+ const allowedCookies = filterAllowedCookies(browser_session, allCookies);
475
562
  const cookies = url
476
- ? allCookies.filter((cookie) => cookieMatchesUrl(cookie, url))
477
- : allCookies;
563
+ ? allowedCookies.filter((cookie) => cookieMatchesUrl(cookie, url))
564
+ : allowedCookies;
478
565
  const filePath = path.resolve(file);
479
- await fsp.writeFile(filePath, JSON.stringify(cookies, null, 2));
566
+ await writePrivateJsonFile(filePath, cookies);
480
567
  return { file: filePath, count: cookies.length };
481
568
  }
482
569
  if (action === 'cookies_import') {
@@ -504,8 +591,11 @@ export class SkillCliServer {
504
591
  }
505
592
  return typedCookie;
506
593
  });
507
- await browser_session.browser_context.addCookies(importedCookies);
508
- return { file: filePath, imported: importedCookies.length };
594
+ const allowedCookies = filterAllowedCookies(browser_session, importedCookies);
595
+ if (allowedCookies.length > 0) {
596
+ await browser_session.browser_context.addCookies(allowedCookies);
597
+ }
598
+ return { file: filePath, imported: allowedCookies.length };
509
599
  }
510
600
  if (action === 'close') {
511
601
  await this.registry.close_session(sessionName);
@@ -48,6 +48,7 @@ export declare class TunnelManager {
48
48
  constructor(options?: TunnelManagerOptions);
49
49
  private get_tunnel_file;
50
50
  private get_tunnel_log_file;
51
+ private ensure_tunnel_dir;
51
52
  private save_tunnel_info;
52
53
  private load_tunnel_info;
53
54
  get_binary_path(): string;
@@ -42,9 +42,18 @@ export class TunnelManager {
42
42
  get_tunnel_log_file(port) {
43
43
  return path.join(this.tunnel_dir, `${port}.log`);
44
44
  }
45
+ ensure_tunnel_dir() {
46
+ fs.mkdirSync(this.tunnel_dir, { recursive: true, mode: 0o700 });
47
+ if (process.platform !== 'win32') {
48
+ fs.chmodSync(this.tunnel_dir, 0o700);
49
+ }
50
+ }
45
51
  save_tunnel_info(port, pid, url) {
46
- fs.mkdirSync(this.tunnel_dir, { recursive: true });
47
- fs.writeFileSync(this.get_tunnel_file(port), JSON.stringify({ port, pid, url }), 'utf-8');
52
+ this.ensure_tunnel_dir();
53
+ fs.writeFileSync(this.get_tunnel_file(port), JSON.stringify({ port, pid, url }), { encoding: 'utf-8', mode: 0o600 });
54
+ if (process.platform !== 'win32') {
55
+ fs.chmodSync(this.get_tunnel_file(port), 0o600);
56
+ }
48
57
  }
49
58
  load_tunnel_info(port) {
50
59
  const filePath = this.get_tunnel_file(port);
@@ -122,9 +131,12 @@ export class TunnelManager {
122
131
  catch (error) {
123
132
  return { error: error.message };
124
133
  }
125
- fs.mkdirSync(this.tunnel_dir, { recursive: true });
134
+ this.ensure_tunnel_dir();
126
135
  const logPath = this.get_tunnel_log_file(port);
127
- const logFd = fs.openSync(logPath, 'w');
136
+ const logFd = fs.openSync(logPath, 'w', 0o600);
137
+ if (process.platform !== 'win32') {
138
+ fs.chmodSync(logPath, 0o600);
139
+ }
128
140
  try {
129
141
  const child = this.spawn_impl(binaryPath, ['tunnel', '--url', `http://localhost:${port}`], {
130
142
  detached: true,
package/dist/sync/auth.js CHANGED
@@ -10,7 +10,26 @@ const CONFIG_DIR = () => CONFIG.BROWSER_USE_CONFIG_DIR ?? path.join(process.cwd(
10
10
  const DEVICE_ID_PATH = () => path.join(CONFIG_DIR(), 'device_id');
11
11
  const CLOUD_AUTH_PATH = () => path.join(CONFIG_DIR(), 'cloud_auth.json');
12
12
  const ensureDir = () => {
13
- fs.mkdirSync(CONFIG_DIR(), { recursive: true });
13
+ fs.mkdirSync(CONFIG_DIR(), { recursive: true, mode: 0o700 });
14
+ if (process.platform !== 'win32') {
15
+ try {
16
+ fs.chmodSync(CONFIG_DIR(), 0o700);
17
+ }
18
+ catch {
19
+ /* noop */
20
+ }
21
+ }
22
+ };
23
+ const writePrivateFile = (filePath, contents) => {
24
+ fs.writeFileSync(filePath, contents, { encoding: 'utf-8', mode: 0o600 });
25
+ if (process.platform !== 'win32') {
26
+ try {
27
+ fs.chmodSync(filePath, 0o600);
28
+ }
29
+ catch {
30
+ /* noop */
31
+ }
32
+ }
14
33
  };
15
34
  const loadAuthConfig = () => {
16
35
  try {
@@ -28,13 +47,7 @@ const loadAuthConfig = () => {
28
47
  };
29
48
  const saveAuthConfig = (config) => {
30
49
  ensureDir();
31
- fs.writeFileSync(CLOUD_AUTH_PATH(), JSON.stringify(config, null, 2), 'utf-8');
32
- try {
33
- fs.chmodSync(CLOUD_AUTH_PATH(), 0o600);
34
- }
35
- catch {
36
- /* noop */
37
- }
50
+ writePrivateFile(CLOUD_AUTH_PATH(), JSON.stringify(config, null, 2));
38
51
  };
39
52
  export const load_cloud_auth_config = () => loadAuthConfig();
40
53
  export const save_cloud_api_token = (api_token, user_id = TEMP_USER_ID) => {
@@ -60,7 +73,7 @@ const getOrCreateDeviceId = () => {
60
73
  /* continue */
61
74
  }
62
75
  const deviceId = uuid7str();
63
- fs.writeFileSync(DEVICE_ID_PATH(), deviceId, 'utf-8');
76
+ writePrivateFile(DEVICE_ID_PATH(), deviceId);
64
77
  return deviceId;
65
78
  };
66
79
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
@@ -8,6 +8,17 @@ const logger = createLogger('browser_use.telemetry');
8
8
  const POSTHOG_EVENT_SETTINGS = {
9
9
  process_person_profile: true,
10
10
  };
11
+ const chmodPrivate = (target, mode) => {
12
+ if (process.platform === 'win32') {
13
+ return;
14
+ }
15
+ try {
16
+ fs.chmodSync(target, mode);
17
+ }
18
+ catch {
19
+ /* noop */
20
+ }
21
+ };
11
22
  export class ProductTelemetry {
12
23
  client = null;
13
24
  debugLogging;
@@ -68,13 +79,21 @@ export class ProductTelemetry {
68
79
  }
69
80
  try {
70
81
  if (!fs.existsSync(this.userIdFile)) {
71
- fs.mkdirSync(path.dirname(this.userIdFile), { recursive: true });
82
+ fs.mkdirSync(path.dirname(this.userIdFile), {
83
+ recursive: true,
84
+ mode: 0o700,
85
+ });
86
+ chmodPrivate(path.dirname(this.userIdFile), 0o700);
72
87
  this.cachedUserId = uuid7str();
73
- fs.writeFileSync(this.userIdFile, this.cachedUserId, 'utf-8');
88
+ fs.writeFileSync(this.userIdFile, this.cachedUserId, {
89
+ encoding: 'utf-8',
90
+ mode: 0o600,
91
+ });
74
92
  }
75
93
  else {
76
94
  this.cachedUserId = fs.readFileSync(this.userIdFile, 'utf-8');
77
95
  }
96
+ chmodPrivate(this.userIdFile, 0o600);
78
97
  }
79
98
  catch {
80
99
  this.cachedUserId = 'UNKNOWN_USER_ID';
@@ -8,6 +8,29 @@ export class BaseTelemetryEvent {
8
8
  };
9
9
  }
10
10
  }
11
+ const REDACTED_TELEMETRY_VALUE = '<redacted>';
12
+ const redactSequence = (value) => {
13
+ if (!Array.isArray(value)) {
14
+ return value;
15
+ }
16
+ return value.map((entry) => entry == null ? entry : REDACTED_TELEMETRY_VALUE);
17
+ };
18
+ const summarizeActionHistory = (value) => {
19
+ if (!Array.isArray(value)) {
20
+ return value;
21
+ }
22
+ return value.map((step) => {
23
+ if (!Array.isArray(step)) {
24
+ return step;
25
+ }
26
+ return step.map((action) => ({
27
+ action: action && typeof action === 'object'
28
+ ? (Object.keys(action)[0] ?? 'unknown')
29
+ : 'unknown',
30
+ }));
31
+ });
32
+ };
33
+ const redactNullableString = (value) => value ? REDACTED_TELEMETRY_VALUE : null;
11
34
  export class AgentTelemetryEvent extends BaseTelemetryEvent {
12
35
  name = 'agent_event';
13
36
  task;
@@ -39,7 +62,7 @@ export class AgentTelemetryEvent extends BaseTelemetryEvent {
39
62
  judge_impossible_task;
40
63
  constructor(payload) {
41
64
  super();
42
- this.task = payload.task;
65
+ this.task = payload.task ? REDACTED_TELEMETRY_VALUE : '';
43
66
  this.model = payload.model;
44
67
  this.model_provider = payload.model_provider;
45
68
  this.max_steps = payload.max_steps;
@@ -49,9 +72,9 @@ export class AgentTelemetryEvent extends BaseTelemetryEvent {
49
72
  this.source = payload.source;
50
73
  this.cdp_url = payload.cdp_url;
51
74
  this.agent_type = payload.agent_type;
52
- this.action_errors = payload.action_errors;
53
- this.action_history = payload.action_history;
54
- this.urls_visited = payload.urls_visited;
75
+ this.action_errors = redactSequence(payload.action_errors);
76
+ this.action_history = summarizeActionHistory(payload.action_history);
77
+ this.urls_visited = redactSequence(payload.urls_visited);
55
78
  this.steps = payload.steps;
56
79
  this.total_input_tokens = payload.total_input_tokens;
57
80
  this.total_output_tokens = payload.total_output_tokens;
@@ -59,11 +82,11 @@ export class AgentTelemetryEvent extends BaseTelemetryEvent {
59
82
  this.total_tokens = payload.total_tokens;
60
83
  this.total_duration_seconds = payload.total_duration_seconds;
61
84
  this.success = payload.success;
62
- this.final_result_response = payload.final_result_response;
63
- this.error_message = payload.error_message;
85
+ this.final_result_response = redactNullableString(payload.final_result_response);
86
+ this.error_message = redactNullableString(payload.error_message);
64
87
  this.judge_verdict = payload.judge_verdict ?? null;
65
- this.judge_reasoning = payload.judge_reasoning ?? null;
66
- this.judge_failure_reason = payload.judge_failure_reason ?? null;
88
+ this.judge_reasoning = redactNullableString(payload.judge_reasoning);
89
+ this.judge_failure_reason = redactNullableString(payload.judge_failure_reason);
67
90
  this.judge_reached_captcha = payload.judge_reached_captcha ?? null;
68
91
  this.judge_impossible_task = payload.judge_impossible_task ?? null;
69
92
  }
@@ -18,5 +18,5 @@ export const CUSTOM_MODEL_PRICING = {
18
18
  max_output_tokens: null,
19
19
  },
20
20
  };
21
- CUSTOM_MODEL_PRICING['bu-latest'] = CUSTOM_MODEL_PRICING['bu-1-0'];
22
- CUSTOM_MODEL_PRICING.smart = CUSTOM_MODEL_PRICING['bu-1-0'];
21
+ CUSTOM_MODEL_PRICING['bu-latest'] = CUSTOM_MODEL_PRICING['bu-2-0'];
22
+ CUSTOM_MODEL_PRICING.smart = CUSTOM_MODEL_PRICING['bu-2-0'];
@@ -0,0 +1,11 @@
1
+ import type { ModelPricing } from './views.js';
2
+ export declare const OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
3
+ export declare const OPENROUTER_MODELS_CACHE_MS: number;
4
+ type OpenRouterMetadata = Record<string, any>;
5
+ export declare const isOpenRouterPricingModel: (modelName: string) => boolean;
6
+ export declare const resetOpenRouterPricingCacheForTesting: () => void;
7
+ export declare function getOpenRouterModelsMetadata(refresh?: boolean): Promise<Record<string, OpenRouterMetadata>>;
8
+ export declare function getOpenRouterModelMetadata(modelName: string, refresh?: boolean): Promise<OpenRouterMetadata | null>;
9
+ export declare function modelPricingFromOpenRouterMetadata(modelName: string, metadata: OpenRouterMetadata): ModelPricing | null;
10
+ export declare function getOpenRouterModelPricing(modelName: string, refresh?: boolean): Promise<ModelPricing | null>;
11
+ export {};
@@ -0,0 +1,102 @@
1
+ import axios from 'axios';
2
+ import { createLogger } from '../logging-config.js';
3
+ const logger = createLogger('browser_use.tokens.openrouter');
4
+ export const OPENROUTER_MODELS_URL = 'https://openrouter.ai/api/v1/models';
5
+ export const OPENROUTER_MODELS_CACHE_MS = 60 * 60 * 1000;
6
+ let openRouterModelsCache = null;
7
+ let openRouterModelsCacheFetchedAt = 0;
8
+ const floatOrNull = (value) => {
9
+ if (value == null || value === '')
10
+ return null;
11
+ const parsed = Number(value);
12
+ return Number.isFinite(parsed) ? parsed : null;
13
+ };
14
+ const intOrNull = (value) => {
15
+ if (value == null || value === '')
16
+ return null;
17
+ const parsed = Number(value);
18
+ return Number.isFinite(parsed) ? Math.trunc(parsed) : null;
19
+ };
20
+ const normalizeOpenRouterModelId = (modelName) => {
21
+ let modelId = modelName;
22
+ if (modelId.startsWith('openrouter/')) {
23
+ modelId = modelId.slice('openrouter/'.length);
24
+ }
25
+ else if (modelId.startsWith('openrouter-')) {
26
+ modelId = modelId.slice('openrouter-'.length);
27
+ }
28
+ return modelId.includes('/') ? modelId : null;
29
+ };
30
+ export const isOpenRouterPricingModel = (modelName) => modelName.startsWith('openrouter/') || modelName.startsWith('openrouter-');
31
+ export const resetOpenRouterPricingCacheForTesting = () => {
32
+ openRouterModelsCache = null;
33
+ openRouterModelsCacheFetchedAt = 0;
34
+ };
35
+ export async function getOpenRouterModelsMetadata(refresh = false) {
36
+ const now = Date.now();
37
+ if (!refresh &&
38
+ openRouterModelsCache &&
39
+ now - openRouterModelsCacheFetchedAt < OPENROUTER_MODELS_CACHE_MS) {
40
+ return openRouterModelsCache;
41
+ }
42
+ try {
43
+ const response = await axios.get(OPENROUTER_MODELS_URL, { timeout: 30_000 });
44
+ const models = Array.isArray(response.data?.data) ? response.data.data : [];
45
+ openRouterModelsCache = {};
46
+ for (const model of models) {
47
+ if (model &&
48
+ typeof model === 'object' &&
49
+ typeof model.id === 'string') {
50
+ openRouterModelsCache[model.id] =
51
+ model;
52
+ }
53
+ }
54
+ openRouterModelsCacheFetchedAt = now;
55
+ return openRouterModelsCache;
56
+ }
57
+ catch (error) {
58
+ logger.debug(`Failed to fetch OpenRouter pricing: ${error.message}`);
59
+ return openRouterModelsCache ?? {};
60
+ }
61
+ }
62
+ export async function getOpenRouterModelMetadata(modelName, refresh = false) {
63
+ const modelId = normalizeOpenRouterModelId(modelName);
64
+ if (!modelId) {
65
+ return null;
66
+ }
67
+ const models = await getOpenRouterModelsMetadata(refresh);
68
+ return models[modelId] ?? null;
69
+ }
70
+ export function modelPricingFromOpenRouterMetadata(modelName, metadata) {
71
+ const pricing = metadata.pricing;
72
+ if (!pricing || typeof pricing !== 'object') {
73
+ return null;
74
+ }
75
+ const inputCost = floatOrNull(pricing.prompt);
76
+ const outputCost = floatOrNull(pricing.completion);
77
+ if (inputCost == null && outputCost == null) {
78
+ return null;
79
+ }
80
+ const contextLength = intOrNull(metadata.context_length);
81
+ const topProvider = metadata.top_provider;
82
+ const maxOutputTokens = topProvider && typeof topProvider === 'object'
83
+ ? intOrNull(topProvider.max_completion_tokens)
84
+ : null;
85
+ return {
86
+ model: modelName,
87
+ input_cost_per_token: inputCost,
88
+ output_cost_per_token: outputCost,
89
+ cache_read_input_token_cost: floatOrNull(pricing.input_cache_read),
90
+ cache_creation_input_token_cost: floatOrNull(pricing.input_cache_write),
91
+ max_tokens: contextLength,
92
+ max_input_tokens: contextLength,
93
+ max_output_tokens: maxOutputTokens,
94
+ };
95
+ }
96
+ export async function getOpenRouterModelPricing(modelName, refresh = false) {
97
+ const metadata = await getOpenRouterModelMetadata(modelName, refresh);
98
+ if (!metadata) {
99
+ return null;
100
+ }
101
+ return modelPricingFromOpenRouterMetadata(modelName, metadata);
102
+ }
@@ -7,6 +7,7 @@ import { createLogger } from '../logging-config.js';
7
7
  import { create_task_with_error_handling } from '../utils.js';
8
8
  import { CUSTOM_MODEL_PRICING } from './custom-pricing.js';
9
9
  import { MODEL_TO_LITELLM } from './mappings.js';
10
+ import { getOpenRouterModelPricing, isOpenRouterPricingModel, } from './openrouter-pricing.js';
10
11
  const logger = createLogger('browser_use.tokens');
11
12
  const costLogger = createLogger('browser_use.tokens.cost');
12
13
  const PRICING_URL = 'https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json';
@@ -263,25 +264,28 @@ export class TokenCost {
263
264
  max_output_tokens: customPricing.max_output_tokens ?? null,
264
265
  };
265
266
  }
266
- await this.ensurePricingLoaded();
267
- if (!this.pricingData) {
268
- return null;
267
+ if (isOpenRouterPricingModel(modelName)) {
268
+ const openRouterPricing = await getOpenRouterModelPricing(modelName);
269
+ if (openRouterPricing) {
270
+ return openRouterPricing;
271
+ }
269
272
  }
273
+ await this.ensurePricingLoaded();
270
274
  const litellmModelName = MODEL_TO_LITELLM[modelName] ?? modelName;
271
- const pricing = this.pricingData[litellmModelName];
272
- if (!pricing) {
273
- return null;
275
+ const pricing = this.pricingData?.[litellmModelName];
276
+ if (pricing) {
277
+ return {
278
+ model: modelName,
279
+ input_cost_per_token: pricing.input_cost_per_token ?? null,
280
+ output_cost_per_token: pricing.output_cost_per_token ?? null,
281
+ cache_read_input_token_cost: pricing.cache_read_input_token_cost ?? null,
282
+ cache_creation_input_token_cost: pricing.cache_creation_input_token_cost ?? null,
283
+ max_tokens: pricing.max_tokens ?? null,
284
+ max_input_tokens: pricing.max_input_tokens ?? null,
285
+ max_output_tokens: pricing.max_output_tokens ?? null,
286
+ };
274
287
  }
275
- return {
276
- model: modelName,
277
- input_cost_per_token: pricing.input_cost_per_token ?? null,
278
- output_cost_per_token: pricing.output_cost_per_token ?? null,
279
- cache_read_input_token_cost: pricing.cache_read_input_token_cost ?? null,
280
- cache_creation_input_token_cost: pricing.cache_creation_input_token_cost ?? null,
281
- max_tokens: pricing.max_tokens ?? null,
282
- max_input_tokens: pricing.max_input_tokens ?? null,
283
- max_output_tokens: pricing.max_output_tokens ?? null,
284
- };
288
+ return await getOpenRouterModelPricing(modelName);
285
289
  }
286
290
  get_usage_tokens_for_model(model) {
287
291
  const filtered = this.usageHistory.filter((entry) => entry.model === model);