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
package/dist/cli.js CHANGED
@@ -27,11 +27,14 @@ import { ChatVercel } from './llm/vercel/chat.js';
27
27
  import { ChatAnthropicBedrock } from './llm/aws/chat-anthropic.js';
28
28
  import { ChatBedrockConverse } from './llm/aws/chat-bedrock.js';
29
29
  import { ChatBrowserUse } from './llm/browser-use/chat.js';
30
+ import { ChatCodex } from './llm/codex/chat.js';
31
+ import { clearCodexTokens, getCodexAuthStatus, loginAndSaveCodexDeviceCode, saveImportedCodexCliTokens, } from './llm/codex/auth.js';
30
32
  import { MCPServer } from './mcp/server.js';
31
33
  import { get_browser_use_version } from './utils.js';
32
34
  import { setupLogging } from './logging-config.js';
33
35
  import { get_tunnel_manager } from './skill-cli/tunnel.js';
34
36
  import { DeviceAuthClient, save_cloud_api_token } from './sync/auth.js';
37
+ import { isMainModule } from './entrypoint.js';
35
38
  import dotenv from 'dotenv';
36
39
  dotenv.config({ quiet: true });
37
40
  const require = createRequire(import.meta.url);
@@ -44,6 +47,8 @@ const CLI_PROVIDER_ALIASES = {
44
47
  groq: 'groq',
45
48
  openrouter: 'openrouter',
46
49
  azure: 'azure',
50
+ codex: 'codex',
51
+ 'openai-codex': 'codex',
47
52
  mistral: 'mistral',
48
53
  cerebras: 'cerebras',
49
54
  vercel: 'vercel',
@@ -81,7 +86,7 @@ const parseProvider = (value) => {
81
86
  const normalized = value.trim().toLowerCase();
82
87
  const provider = CLI_PROVIDER_ALIASES[normalized];
83
88
  if (!provider) {
84
- throw new Error(`Unsupported provider "${value}". Supported values: openai, anthropic, google, deepseek, groq, openrouter, azure, mistral, cerebras, vercel, oci, ollama, browser-use, aws, aws-anthropic.`);
89
+ throw new Error(`Unsupported provider "${value}". Supported values: openai, anthropic, google, deepseek, groq, openrouter, azure, codex, mistral, cerebras, vercel, oci, ollama, browser-use, aws, aws-anthropic.`);
85
90
  }
86
91
  return provider;
87
92
  };
@@ -301,6 +306,21 @@ export const getCliHistoryPath = (configDir) => {
301
306
  path.join(os.homedir(), '.config', 'browseruse');
302
307
  return path.join(baseDir, 'command_history.json');
303
308
  };
309
+ const chmodPrivatePath = async (targetPath, mode) => {
310
+ if (process.platform === 'win32') {
311
+ return;
312
+ }
313
+ try {
314
+ await fs.chmod(targetPath, mode);
315
+ }
316
+ catch {
317
+ /* best effort */
318
+ }
319
+ };
320
+ const ensurePrivateDirectory = async (dirPath) => {
321
+ await fs.mkdir(dirPath, { recursive: true, mode: 0o700 });
322
+ await chmodPrivatePath(dirPath, 0o700);
323
+ };
304
324
  export const loadCliHistory = async (historyPath = getCliHistoryPath()) => {
305
325
  try {
306
326
  const raw = await fs.readFile(historyPath, 'utf-8');
@@ -320,8 +340,12 @@ export const loadCliHistory = async (historyPath = getCliHistoryPath()) => {
320
340
  };
321
341
  export const saveCliHistory = async (history, historyPath = getCliHistoryPath()) => {
322
342
  const normalized = normalizeCliHistory(history);
323
- await fs.mkdir(path.dirname(historyPath), { recursive: true });
324
- await fs.writeFile(historyPath, JSON.stringify(normalized, null, 2), 'utf-8');
343
+ await ensurePrivateDirectory(path.dirname(historyPath));
344
+ await fs.writeFile(historyPath, JSON.stringify(normalized, null, 2), {
345
+ encoding: 'utf-8',
346
+ mode: 0o600,
347
+ });
348
+ await chmodPrivatePath(historyPath, 0o600);
325
349
  };
326
350
  export const shouldStartInteractiveMode = (task, options = {}) => {
327
351
  const forceInteractive = options.forceInteractive ??
@@ -364,6 +388,9 @@ const inferProviderFromModel = (model) => {
364
388
  if (lower.startsWith('azure:')) {
365
389
  return 'azure';
366
390
  }
391
+ if (lower.startsWith('codex:') || lower.startsWith('openai-codex:')) {
392
+ return 'codex';
393
+ }
367
394
  if (lower.startsWith('mistral:')) {
368
395
  return 'mistral';
369
396
  }
@@ -422,6 +449,12 @@ const normalizeModelValue = (model, provider) => {
422
449
  if (provider === 'azure' && lower.startsWith('azure:')) {
423
450
  return model.slice('azure:'.length);
424
451
  }
452
+ if (provider === 'codex' && lower.startsWith('codex:')) {
453
+ return model.slice('codex:'.length);
454
+ }
455
+ if (provider === 'codex' && lower.startsWith('openai-codex:')) {
456
+ return model.slice('openai-codex:'.length);
457
+ }
425
458
  if (provider === 'mistral' && lower.startsWith('mistral:')) {
426
459
  return model.slice('mistral:'.length);
427
460
  }
@@ -477,6 +510,8 @@ const getDefaultModelForProvider = (provider) => {
477
510
  return 'openai/gpt-5-mini';
478
511
  case 'azure':
479
512
  return 'gpt-4o';
513
+ case 'codex':
514
+ return 'gpt-5.5';
480
515
  case 'mistral':
481
516
  return 'mistral-large-latest';
482
517
  case 'cerebras':
@@ -525,6 +560,8 @@ const createLlmForProvider = (provider, model) => {
525
560
  requireEnv('AZURE_OPENAI_API_KEY');
526
561
  requireEnv('AZURE_OPENAI_ENDPOINT');
527
562
  return new ChatAzure(model);
563
+ case 'codex':
564
+ return new ChatCodex({ model });
528
565
  case 'mistral':
529
566
  return new ChatMistral({
530
567
  model,
@@ -577,7 +614,7 @@ export const getLlmFromCliArgs = (args) => {
577
614
  }
578
615
  const provider = args.provider ?? inferredProvider;
579
616
  if (!provider) {
580
- throw new Error(`Cannot infer provider from model "${args.model}". Provide --provider or use a supported model prefix: gpt*/o*, claude*, gemini*, deepseek*, groq:, openrouter:, azure:, mistral:, cerebras:, vercel:, oci:, ollama:, browser-use:, bu-*, bedrock:.`);
617
+ throw new Error(`Cannot infer provider from model "${args.model}". Provide --provider or use a supported model prefix: gpt*/o*, claude*, gemini*, deepseek*, groq:, openrouter:, azure:, codex:, mistral:, cerebras:, vercel:, oci:, ollama:, browser-use:, bu-*, bedrock:.`);
581
618
  }
582
619
  const normalizedModel = normalizeModelValue(args.model, provider);
583
620
  return createLlmForProvider(provider, normalizedModel);
@@ -778,6 +815,7 @@ export const getCliUsage = () => `Usage:
778
815
  browser-use doctor
779
816
  browser-use install
780
817
  browser-use setup [--mode <local|remote|full>]
818
+ browser-use auth codex <login|status|logout|import>
781
819
  browser-use tunnel <port>
782
820
  browser-use task <list|status|stop|logs>
783
821
  browser-use session <list|get|stop|create|share>
@@ -794,8 +832,8 @@ Options:
794
832
  --mcp Run as MCP server
795
833
  --json Output command results as JSON when supported
796
834
  -y, --yes Skip optional setup prompts where supported
797
- --provider <name> Force provider (openai|anthropic|google|deepseek|groq|openrouter|azure|mistral|cerebras|vercel|oci|ollama|browser-use|aws|aws-anthropic)
798
- --model <model> Set model (e.g., gpt-5-mini, claude-4-sonnet, gemini-2.5-pro)
835
+ --provider <name> Force provider (openai|anthropic|google|deepseek|groq|openrouter|azure|codex|mistral|cerebras|vercel|oci|ollama|browser-use|aws|aws-anthropic)
836
+ --model <model> Set model (e.g., gpt-5-mini, codex:gpt-5.5, claude-4-sonnet)
799
837
  -p, --prompt <task> Run a single task
800
838
  --mode <name> Setup mode for setup command (local|remote|full)
801
839
  --api-key <value> Browser Use API key for setup or cloud operations
@@ -828,6 +866,141 @@ export const runInstallCommand = (options = {}) => {
828
866
  const writeLine = (stream, value) => {
829
867
  stream.write(`${value}\n`);
830
868
  };
869
+ const formatCodexAuthStatus = (status) => {
870
+ if (!status.authenticated) {
871
+ return [
872
+ 'Codex auth: not authenticated',
873
+ `Auth store: ${status.auth_store_path}`,
874
+ 'Run `browser-use auth codex login` to authenticate.',
875
+ ].join('\n');
876
+ }
877
+ return [
878
+ 'Codex auth: authenticated',
879
+ `Provider: ${status.provider}`,
880
+ `Base URL: ${status.base_url}`,
881
+ `Source: ${status.source ?? 'unknown'}`,
882
+ `Last refresh: ${status.last_refresh ?? 'unknown'}`,
883
+ `Access token expiring: ${String(status.access_token_expiring)}`,
884
+ `Auth store: ${status.auth_store_path}`,
885
+ ].join('\n');
886
+ };
887
+ const parseCodexAuthArgs = (argv) => {
888
+ const flags = {
889
+ json: false,
890
+ force: false,
891
+ importFromCodexCli: false,
892
+ parts: [],
893
+ };
894
+ for (const arg of argv) {
895
+ if (arg === '--json') {
896
+ flags.json = true;
897
+ continue;
898
+ }
899
+ if (arg === '--force') {
900
+ flags.force = true;
901
+ continue;
902
+ }
903
+ if (arg === '--import') {
904
+ flags.importFromCodexCli = true;
905
+ continue;
906
+ }
907
+ if (arg.startsWith('-')) {
908
+ throw new Error(`Unknown auth option: ${arg}`);
909
+ }
910
+ flags.parts.push(arg);
911
+ }
912
+ return flags;
913
+ };
914
+ export const runAuthCommand = async (argv, options = {}) => {
915
+ const output = options.stdout ?? process.stdout;
916
+ const errorOutput = options.stderr ?? process.stderr;
917
+ const loginDeviceCode = options.login_device_code ?? loginAndSaveCodexDeviceCode;
918
+ const importCodexCli = options.import_codex_cli ?? saveImportedCodexCliTokens;
919
+ try {
920
+ const flags = parseCodexAuthArgs(argv);
921
+ const jsonOutput = options.json_output ?? flags.json;
922
+ const provider = flags.parts[0] ?? 'codex';
923
+ const action = flags.parts[1] ?? (flags.importFromCodexCli ? 'import' : 'status');
924
+ if (provider !== 'codex' && provider !== 'openai-codex') {
925
+ throw new Error('Usage: browser-use auth codex <login|status|logout|import>');
926
+ }
927
+ const authOptions = {
928
+ configDir: options.configDir,
929
+ authStorePath: options.authStorePath,
930
+ };
931
+ if (action === 'status') {
932
+ const status = await getCodexAuthStatus(authOptions);
933
+ if (jsonOutput) {
934
+ writeLine(output, JSON.stringify(status, null, 2));
935
+ }
936
+ else {
937
+ writeLine(output, formatCodexAuthStatus(status));
938
+ }
939
+ return status.authenticated ? 0 : 1;
940
+ }
941
+ if (action === 'logout') {
942
+ await clearCodexTokens(authOptions);
943
+ const status = await getCodexAuthStatus(authOptions);
944
+ if (jsonOutput) {
945
+ writeLine(output, JSON.stringify(status, null, 2));
946
+ }
947
+ else {
948
+ writeLine(output, 'Codex auth cleared from browser-use auth store.');
949
+ }
950
+ return 0;
951
+ }
952
+ if (action === 'import' || flags.importFromCodexCli) {
953
+ const imported = await importCodexCli(authOptions);
954
+ if (!imported) {
955
+ writeLine(errorOutput, 'No valid Codex CLI credentials found. Run `browser-use auth codex login` for a separate browser-use session.');
956
+ return 1;
957
+ }
958
+ const status = await getCodexAuthStatus(authOptions);
959
+ if (jsonOutput) {
960
+ writeLine(output, JSON.stringify(status, null, 2));
961
+ }
962
+ else {
963
+ writeLine(output, 'Imported Codex CLI credentials into browser-use auth store.');
964
+ writeLine(output, 'browser-use will refresh its own copy and will not write ~/.codex/auth.json.');
965
+ writeLine(output, 'For the cleanest separation from Codex CLI, prefer `browser-use auth codex login --force`.');
966
+ }
967
+ return 0;
968
+ }
969
+ if (action === 'login') {
970
+ if (!flags.force) {
971
+ const status = await getCodexAuthStatus(authOptions);
972
+ if (status.authenticated) {
973
+ if (jsonOutput) {
974
+ writeLine(output, JSON.stringify(status, null, 2));
975
+ }
976
+ else {
977
+ writeLine(output, 'Existing browser-use Codex credentials found.');
978
+ writeLine(output, 'Use `browser-use auth codex login --force` to create a fresh session.');
979
+ }
980
+ return 0;
981
+ }
982
+ }
983
+ await loginDeviceCode({
984
+ ...authOptions,
985
+ stdout: (jsonOutput ? errorOutput : output),
986
+ });
987
+ const status = await getCodexAuthStatus(authOptions);
988
+ if (jsonOutput) {
989
+ writeLine(output, JSON.stringify(status, null, 2));
990
+ }
991
+ else {
992
+ writeLine(output, 'Codex login successful.');
993
+ writeLine(output, `Auth store: ${status.auth_store_path}`);
994
+ }
995
+ return 0;
996
+ }
997
+ throw new Error('Usage: browser-use auth codex <login|status|logout|import>');
998
+ }
999
+ catch (error) {
1000
+ writeLine(errorOutput, `Error: ${error.message}`);
1001
+ return 1;
1002
+ }
1003
+ };
831
1004
  const parseTunnelPort = (value) => {
832
1005
  const port = Number.parseInt(String(value ?? ''), 10);
833
1006
  if (!Number.isFinite(port) || port <= 0) {
@@ -2106,7 +2279,8 @@ export const extractPrefixedSubcommand = (argv) => {
2106
2279
  if (command !== 'run' &&
2107
2280
  command !== 'task' &&
2108
2281
  command !== 'session' &&
2109
- command !== 'profile') {
2282
+ command !== 'profile' &&
2283
+ command !== 'auth') {
2110
2284
  return null;
2111
2285
  }
2112
2286
  return {
@@ -2551,6 +2725,13 @@ export async function main(argv = process.argv.slice(2)) {
2551
2725
  }
2552
2726
  return;
2553
2727
  }
2728
+ if (prefixedSubcommand.command === 'auth') {
2729
+ const exitCode = await runAuthCommand(subcommandArgv);
2730
+ if (exitCode !== 0) {
2731
+ process.exit(exitCode);
2732
+ }
2733
+ return;
2734
+ }
2554
2735
  const exitCode = await runProfileCommand(subcommandArgv);
2555
2736
  if (exitCode !== 0) {
2556
2737
  process.exit(exitCode);
@@ -2661,6 +2842,6 @@ export async function main(argv = process.argv.slice(2)) {
2661
2842
  process.exit(1);
2662
2843
  }
2663
2844
  }
2664
- if (import.meta.url === `file://${process.argv[1]}`) {
2665
- main();
2845
+ if (isMainModule(import.meta.url)) {
2846
+ void main();
2666
2847
  }
@@ -1,7 +1,46 @@
1
1
  const buildExpression = (source, args) => `(${source})(${args.map((arg) => JSON.stringify(arg)).join(',')})`;
2
+ const hasDomainRestrictions = (browser_session) => {
3
+ const checker = browser_session._has_url_access_restrictions;
4
+ if (typeof checker === 'function') {
5
+ try {
6
+ return Boolean(checker.call(browser_session));
7
+ }
8
+ catch {
9
+ return true;
10
+ }
11
+ }
12
+ const profile = browser_session.browser_profile;
13
+ const hasEntries = (value) => Array.isArray(value)
14
+ ? value.length > 0
15
+ : value instanceof Set && value.size > 0;
16
+ return (hasEntries(profile?.allowed_domains) ||
17
+ hasEntries(profile?.prohibited_domains) ||
18
+ Boolean(profile?.block_ip_addresses));
19
+ };
20
+ const createSafeBrowserFacade = (browser_session) => Object.freeze({
21
+ navigate_to: browser_session.navigate_to.bind(browser_session),
22
+ navigate: browser_session.navigate.bind(browser_session),
23
+ create_new_tab: browser_session.create_new_tab.bind(browser_session),
24
+ go_back: browser_session.go_back.bind(browser_session),
25
+ go_forward: browser_session.go_forward.bind(browser_session),
26
+ refresh: browser_session.refresh.bind(browser_session),
27
+ wait: browser_session.wait.bind(browser_session),
28
+ send_keys: browser_session.send_keys.bind(browser_session),
29
+ click_coordinates: browser_session.click_coordinates.bind(browser_session),
30
+ scroll: browser_session.scroll.bind(browser_session),
31
+ scroll_to_text: browser_session.scroll_to_text.bind(browser_session),
32
+ get_browser_state_with_recovery: browser_session.get_browser_state_with_recovery.bind(browser_session),
33
+ get_page_info: browser_session.get_page_info.bind(browser_session),
34
+ get_page_html: browser_session.get_page_html.bind(browser_session),
35
+ execute_javascript: browser_session.execute_javascript.bind(browser_session),
36
+ take_screenshot: browser_session.take_screenshot.bind(browser_session),
37
+ get_cookies: () => browser_session.get_cookies(),
38
+ });
2
39
  export const create_namespace = (browser_session, options = {}) => {
3
40
  const namespace = options.namespace ?? {};
4
- namespace.browser = browser_session;
41
+ namespace.browser = hasDomainRestrictions(browser_session)
42
+ ? createSafeBrowserFacade(browser_session)
43
+ : browser_session;
5
44
  namespace.navigate = async (url, init = {}) => {
6
45
  await browser_session.navigate_to(url, init);
7
46
  };
@@ -55,16 +94,22 @@ export const create_namespace = (browser_session, options = {}) => {
55
94
  };
56
95
  namespace.evaluate = async (code, ...args) => {
57
96
  const page = await browser_session.get_current_page();
58
- if (!page) {
97
+ if (!page?.evaluate) {
59
98
  throw new Error('No active page for evaluate');
60
99
  }
61
- if (typeof code === 'function') {
62
- return page.evaluate(code, ...args);
100
+ await browser_session.validate_page_after_action(page);
101
+ try {
102
+ if (typeof code === 'function') {
103
+ return await page.evaluate(code, ...args);
104
+ }
105
+ if (args.length === 0) {
106
+ return await page.evaluate(code);
107
+ }
108
+ return await page.evaluate(buildExpression(code, args));
63
109
  }
64
- if (args.length === 0) {
65
- return page.evaluate(code);
110
+ finally {
111
+ await browser_session.validate_page_after_action(page);
66
112
  }
67
- return page.evaluate(buildExpression(code, args));
68
113
  };
69
114
  namespace.done = (result = null, success = true) => {
70
115
  namespace._task_done = true;
@@ -1,5 +1,17 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ const chmodPrivateFile = (filePath) => {
4
+ if (process.platform !== 'win32') {
5
+ fs.chmodSync(filePath, 0o600);
6
+ }
7
+ };
8
+ const ensurePrivateDirectoryIfCreated = (dirPath) => {
9
+ const existed = fs.existsSync(dirPath);
10
+ fs.mkdirSync(dirPath, { recursive: true, mode: 0o700 });
11
+ if (!existed && process.platform !== 'win32') {
12
+ fs.chmodSync(dirPath, 0o700);
13
+ }
14
+ };
3
15
  export const export_to_ipynb = (agent, output_path) => {
4
16
  const notebook = {
5
17
  nbformat: 4,
@@ -37,8 +49,12 @@ export const export_to_ipynb = (agent, output_path) => {
37
49
  }),
38
50
  };
39
51
  const resolved = path.resolve(output_path);
40
- fs.mkdirSync(path.dirname(resolved), { recursive: true });
41
- fs.writeFileSync(resolved, JSON.stringify(notebook, null, 2), 'utf-8');
52
+ ensurePrivateDirectoryIfCreated(path.dirname(resolved));
53
+ fs.writeFileSync(resolved, JSON.stringify(notebook, null, 2), {
54
+ encoding: 'utf-8',
55
+ mode: 0o600,
56
+ });
57
+ chmodPrivateFile(resolved);
42
58
  return resolved;
43
59
  };
44
60
  export const session_to_python_script = (agent) => {
@@ -65,6 +65,7 @@ export class CodeAgent {
65
65
  };
66
66
  }
67
67
  const page = await this.browser_session.get_current_page();
68
+ await this.browser_session.validate_page_after_action(page ?? null);
68
69
  const state = new CodeAgentState({
69
70
  url: typeof page?.url === 'function' ? page.url() : null,
70
71
  title: typeof page?.title === 'function' ? await page.title() : null,
package/dist/config.js CHANGED
@@ -65,7 +65,28 @@ export const is_running_in_docker = () => {
65
65
  docker_cache = false;
66
66
  return false;
67
67
  };
68
- const ensure_dir = (target) => fs.mkdirSync(target, { recursive: true });
68
+ const chmod_private = (target, mode) => {
69
+ if (process.platform === 'win32') {
70
+ return;
71
+ }
72
+ try {
73
+ fs.chmodSync(target, mode);
74
+ }
75
+ catch {
76
+ /* noop */
77
+ }
78
+ };
79
+ const ensure_dir = (target) => {
80
+ fs.mkdirSync(target, { recursive: true, mode: 0o700 });
81
+ chmod_private(target, 0o700);
82
+ };
83
+ const write_private_config_file = (config_path, config) => {
84
+ fs.writeFileSync(config_path, JSON.stringify(config, null, 2), {
85
+ encoding: 'utf-8',
86
+ mode: 0o600,
87
+ });
88
+ chmod_private(config_path, 0o600);
89
+ };
69
90
  class OldConfig {
70
91
  _dirs_created = false;
71
92
  get BROWSER_USE_LOGGING_LEVEL() {
@@ -377,24 +398,25 @@ const load_and_migrate_config = (config_path) => {
377
398
  const parent = path.dirname(config_path);
378
399
  ensure_dir(parent);
379
400
  const fresh = create_default_config();
380
- fs.writeFileSync(config_path, JSON.stringify(fresh, null, 2), 'utf-8');
401
+ write_private_config_file(config_path, fresh);
381
402
  return fresh;
382
403
  }
383
404
  try {
384
405
  const raw = JSON.parse(fs.readFileSync(config_path, 'utf-8'));
385
406
  if (looks_like_new_format(raw)) {
407
+ chmod_private(config_path, 0o600);
386
408
  return sanitize_db_config(raw);
387
409
  }
388
410
  logger.debug(`Old config format detected at ${config_path}, creating fresh config`);
389
411
  const fresh = create_default_config();
390
- fs.writeFileSync(config_path, JSON.stringify(fresh, null, 2), 'utf-8');
412
+ write_private_config_file(config_path, fresh);
391
413
  return fresh;
392
414
  }
393
415
  catch (error) {
394
416
  logger.error(`Failed to load config from ${config_path}: ${error.message}, creating fresh config`);
395
417
  const fresh = create_default_config();
396
418
  try {
397
- fs.writeFileSync(config_path, JSON.stringify(fresh, null, 2), 'utf-8');
419
+ write_private_config_file(config_path, fresh);
398
420
  }
399
421
  catch (write_error) {
400
422
  logger.error(`Failed to write fresh config: ${write_error.message}`);
@@ -0,0 +1,9 @@
1
+ export declare const coerceActionTimeoutSeconds: (value: number | null | undefined) => number;
2
+ export declare class ActionTimeoutError extends Error {
3
+ readonly actionName: string;
4
+ readonly timeoutSeconds: number;
5
+ readonly isBrowserUseActionTimeout = true;
6
+ constructor(actionName: string, timeoutSeconds: number);
7
+ }
8
+ export declare const isActionTimeoutError: (error: unknown) => error is ActionTimeoutError;
9
+ export declare function runActionWithTimeout<T>(actionName: string, actionTimeoutSeconds: number | null | undefined, parentSignal: AbortSignal | null | undefined, execute: (signal: AbortSignal) => Promise<T>): Promise<T>;
@@ -0,0 +1,95 @@
1
+ import { createLogger } from '../logging-config.js';
2
+ const logger = createLogger('browser_use.controller.action_timeout');
3
+ const ACTION_TIMEOUT_FALLBACK_SECONDS = 180;
4
+ const ACTION_TIMEOUT_ENV = 'BROWSER_USE_ACTION_TIMEOUT_S';
5
+ const createAbortError = (reason) => {
6
+ if (reason instanceof Error && reason.name === 'AbortError') {
7
+ return reason;
8
+ }
9
+ const error = new Error(reason instanceof Error ? reason.message : 'Operation aborted');
10
+ error.name = 'AbortError';
11
+ if (reason !== undefined) {
12
+ error.cause = reason;
13
+ }
14
+ return error;
15
+ };
16
+ const parseEnvActionTimeoutSeconds = () => {
17
+ const raw = process.env[ACTION_TIMEOUT_ENV];
18
+ if (raw == null || raw === '') {
19
+ return ACTION_TIMEOUT_FALLBACK_SECONDS;
20
+ }
21
+ const parsed = Number(raw);
22
+ if (!Number.isFinite(parsed) || parsed <= 0) {
23
+ logger.warning(`Invalid ${ACTION_TIMEOUT_ENV}=${JSON.stringify(raw)}; falling back to ${ACTION_TIMEOUT_FALLBACK_SECONDS}s`);
24
+ return ACTION_TIMEOUT_FALLBACK_SECONDS;
25
+ }
26
+ return parsed;
27
+ };
28
+ export const coerceActionTimeoutSeconds = (value) => {
29
+ if (value == null) {
30
+ return parseEnvActionTimeoutSeconds();
31
+ }
32
+ if (!Number.isFinite(value) || value <= 0) {
33
+ const fallback = parseEnvActionTimeoutSeconds();
34
+ logger.warning(`Invalid action_timeout=${String(value)}; falling back to ${fallback}s`);
35
+ return fallback;
36
+ }
37
+ return value;
38
+ };
39
+ export class ActionTimeoutError extends Error {
40
+ actionName;
41
+ timeoutSeconds;
42
+ isBrowserUseActionTimeout = true;
43
+ constructor(actionName, timeoutSeconds) {
44
+ super(`Action ${actionName} timed out after ${Math.round(timeoutSeconds)}s. ` +
45
+ 'The browser may be unresponsive (dead CDP WebSocket). Try again or a different approach.');
46
+ this.name = 'TimeoutError';
47
+ this.actionName = actionName;
48
+ this.timeoutSeconds = timeoutSeconds;
49
+ }
50
+ }
51
+ export const isActionTimeoutError = (error) => error instanceof Error &&
52
+ error.name === 'TimeoutError' &&
53
+ error.isBrowserUseActionTimeout === true;
54
+ export async function runActionWithTimeout(actionName, actionTimeoutSeconds, parentSignal, execute) {
55
+ const timeoutSeconds = coerceActionTimeoutSeconds(actionTimeoutSeconds);
56
+ const controller = new AbortController();
57
+ if (parentSignal?.aborted) {
58
+ throw createAbortError(parentSignal.reason);
59
+ }
60
+ let timeoutHandle = null;
61
+ let abortHandler = null;
62
+ try {
63
+ const timeoutPromise = new Promise((_, reject) => {
64
+ timeoutHandle = setTimeout(() => {
65
+ const timeoutError = new ActionTimeoutError(actionName, timeoutSeconds);
66
+ reject(timeoutError);
67
+ controller.abort(timeoutError);
68
+ }, timeoutSeconds * 1000);
69
+ });
70
+ const abortPromise = new Promise((_, reject) => {
71
+ if (!parentSignal) {
72
+ return;
73
+ }
74
+ abortHandler = () => {
75
+ const abortError = createAbortError(parentSignal.reason);
76
+ controller.abort(abortError);
77
+ reject(abortError);
78
+ };
79
+ parentSignal.addEventListener('abort', abortHandler, { once: true });
80
+ });
81
+ return await Promise.race([
82
+ execute(controller.signal),
83
+ timeoutPromise,
84
+ abortPromise,
85
+ ]);
86
+ }
87
+ finally {
88
+ if (timeoutHandle) {
89
+ clearTimeout(timeoutHandle);
90
+ }
91
+ if (abortHandler) {
92
+ parentSignal?.removeEventListener('abort', abortHandler);
93
+ }
94
+ }
95
+ }
@@ -44,6 +44,7 @@ export declare class Registry<Context = unknown> {
44
44
  get_all_actions(): Map<string, RegisteredAction>;
45
45
  execute_action: (...args: any[]) => any;
46
46
  private replace_sensitive_data;
47
+ private redactUrlForLog;
47
48
  private log_sensitive_data_usage;
48
49
  create_action_model(options?: {
49
50
  include_actions?: string[] | null;
@@ -416,11 +416,38 @@ export class Registry {
416
416
  }
417
417
  return processed;
418
418
  }
419
+ redactUrlForLog(url) {
420
+ if (!url || is_new_tab_page(url)) {
421
+ return '';
422
+ }
423
+ try {
424
+ const parsed = new URL(url);
425
+ if (parsed.protocol === 'data:') {
426
+ return 'data:<redacted>';
427
+ }
428
+ if (parsed.protocol === 'blob:') {
429
+ return parsed.origin && parsed.origin !== 'null'
430
+ ? `blob:${parsed.origin}/<redacted>`
431
+ : 'blob:<redacted>';
432
+ }
433
+ return `${parsed.origin}${parsed.pathname}${parsed.search ? '?<redacted>' : ''}${parsed.hash ? '#<redacted>' : ''}`;
434
+ }
435
+ catch {
436
+ const queryIndex = url.indexOf('?');
437
+ const hashIndex = url.indexOf('#');
438
+ const cutoffCandidates = [queryIndex, hashIndex].filter((index) => index >= 0);
439
+ const cutoff = cutoffCandidates.length > 0
440
+ ? Math.min(...cutoffCandidates)
441
+ : url.length;
442
+ return `${url.slice(0, cutoff)}${queryIndex >= 0 ? '?<redacted>' : ''}${hashIndex >= 0 ? '#<redacted>' : ''}`;
443
+ }
444
+ }
419
445
  log_sensitive_data_usage(placeholders, currentUrl) {
420
446
  if (!placeholders.size) {
421
447
  return;
422
448
  }
423
- const urlInfo = currentUrl && !is_new_tab_page(currentUrl) ? ` on ${currentUrl}` : '';
449
+ const redactedUrl = this.redactUrlForLog(currentUrl);
450
+ const urlInfo = redactedUrl ? ` on ${redactedUrl}` : '';
424
451
  logger.info(`🔒 Using sensitive data placeholders: ${Array.from(placeholders).sort().join(', ')}${urlInfo}`);
425
452
  }
426
453
  create_action_model(options = {}) {
@@ -24,6 +24,7 @@ export interface ActParams<Context = unknown> {
24
24
  file_system?: FileSystem | null;
25
25
  context?: Context | null;
26
26
  signal?: AbortSignal | null;
27
+ action_timeout?: number | null;
27
28
  }
28
29
  export declare class Controller<Context = unknown> {
29
30
  registry: Registry<Context>;
@@ -53,6 +54,6 @@ export declare class Controller<Context = unknown> {
53
54
  exclude_action(actionName: string): void;
54
55
  set_coordinate_clicking(enabled: boolean): void;
55
56
  action(description: string, options?: {}): <Params = any>(handler: import("./index.js").RegistryActionHandler<Params, Context>) => any;
56
- act(action: Record<string, unknown>, { browser_session, page_extraction_llm, sensitive_data, available_file_paths, file_system, context, signal, }: ActParams<Context>): Promise<ActionResult>;
57
+ act(action: Record<string, unknown>, { browser_session, page_extraction_llm, sensitive_data, available_file_paths, file_system, context, signal, action_timeout, }: ActParams<Context>): Promise<ActionResult>;
57
58
  }
58
59
  export {};