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.
Files changed (85) hide show
  1. package/README.md +29 -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 +215 -81
  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 +188 -8
  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 +27 -5
  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/registry/views.d.ts +2 -0
  48. package/dist/controller/registry/views.js +44 -17
  49. package/dist/controller/service.d.ts +2 -1
  50. package/dist/controller/service.js +494 -329
  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/observability.js +1 -1
  69. package/dist/screenshots/service.js +25 -2
  70. package/dist/skill-cli/direct.d.ts +4 -1
  71. package/dist/skill-cli/direct.js +260 -64
  72. package/dist/skill-cli/server.d.ts +1 -0
  73. package/dist/skill-cli/server.js +115 -25
  74. package/dist/skill-cli/tunnel.d.ts +1 -0
  75. package/dist/skill-cli/tunnel.js +16 -4
  76. package/dist/sync/auth.js +22 -9
  77. package/dist/telemetry/service.js +21 -2
  78. package/dist/telemetry/views.js +31 -8
  79. package/dist/tokens/custom-pricing.js +2 -2
  80. package/dist/tokens/openrouter-pricing.d.ts +11 -0
  81. package/dist/tokens/openrouter-pricing.js +102 -0
  82. package/dist/tokens/service.js +20 -16
  83. package/dist/utils.d.ts +3 -1
  84. package/dist/utils.js +4 -2
  85. package/package.json +75 -33
package/dist/cli.js CHANGED
@@ -27,13 +27,15 @@ 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';
35
37
  import dotenv from 'dotenv';
36
- dotenv.config();
38
+ dotenv.config({ quiet: true });
37
39
  const require = createRequire(import.meta.url);
38
40
  const CLI_PROVIDER_ALIASES = {
39
41
  openai: 'openai',
@@ -44,6 +46,8 @@ const CLI_PROVIDER_ALIASES = {
44
46
  groq: 'groq',
45
47
  openrouter: 'openrouter',
46
48
  azure: 'azure',
49
+ codex: 'codex',
50
+ 'openai-codex': 'codex',
47
51
  mistral: 'mistral',
48
52
  cerebras: 'cerebras',
49
53
  vercel: 'vercel',
@@ -81,7 +85,7 @@ const parseProvider = (value) => {
81
85
  const normalized = value.trim().toLowerCase();
82
86
  const provider = CLI_PROVIDER_ALIASES[normalized];
83
87
  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.`);
88
+ 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
89
  }
86
90
  return provider;
87
91
  };
@@ -301,6 +305,21 @@ export const getCliHistoryPath = (configDir) => {
301
305
  path.join(os.homedir(), '.config', 'browseruse');
302
306
  return path.join(baseDir, 'command_history.json');
303
307
  };
308
+ const chmodPrivatePath = async (targetPath, mode) => {
309
+ if (process.platform === 'win32') {
310
+ return;
311
+ }
312
+ try {
313
+ await fs.chmod(targetPath, mode);
314
+ }
315
+ catch {
316
+ /* best effort */
317
+ }
318
+ };
319
+ const ensurePrivateDirectory = async (dirPath) => {
320
+ await fs.mkdir(dirPath, { recursive: true, mode: 0o700 });
321
+ await chmodPrivatePath(dirPath, 0o700);
322
+ };
304
323
  export const loadCliHistory = async (historyPath = getCliHistoryPath()) => {
305
324
  try {
306
325
  const raw = await fs.readFile(historyPath, 'utf-8');
@@ -320,8 +339,12 @@ export const loadCliHistory = async (historyPath = getCliHistoryPath()) => {
320
339
  };
321
340
  export const saveCliHistory = async (history, historyPath = getCliHistoryPath()) => {
322
341
  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');
342
+ await ensurePrivateDirectory(path.dirname(historyPath));
343
+ await fs.writeFile(historyPath, JSON.stringify(normalized, null, 2), {
344
+ encoding: 'utf-8',
345
+ mode: 0o600,
346
+ });
347
+ await chmodPrivatePath(historyPath, 0o600);
325
348
  };
326
349
  export const shouldStartInteractiveMode = (task, options = {}) => {
327
350
  const forceInteractive = options.forceInteractive ??
@@ -364,6 +387,9 @@ const inferProviderFromModel = (model) => {
364
387
  if (lower.startsWith('azure:')) {
365
388
  return 'azure';
366
389
  }
390
+ if (lower.startsWith('codex:') || lower.startsWith('openai-codex:')) {
391
+ return 'codex';
392
+ }
367
393
  if (lower.startsWith('mistral:')) {
368
394
  return 'mistral';
369
395
  }
@@ -422,6 +448,12 @@ const normalizeModelValue = (model, provider) => {
422
448
  if (provider === 'azure' && lower.startsWith('azure:')) {
423
449
  return model.slice('azure:'.length);
424
450
  }
451
+ if (provider === 'codex' && lower.startsWith('codex:')) {
452
+ return model.slice('codex:'.length);
453
+ }
454
+ if (provider === 'codex' && lower.startsWith('openai-codex:')) {
455
+ return model.slice('openai-codex:'.length);
456
+ }
425
457
  if (provider === 'mistral' && lower.startsWith('mistral:')) {
426
458
  return model.slice('mistral:'.length);
427
459
  }
@@ -477,6 +509,8 @@ const getDefaultModelForProvider = (provider) => {
477
509
  return 'openai/gpt-5-mini';
478
510
  case 'azure':
479
511
  return 'gpt-4o';
512
+ case 'codex':
513
+ return 'gpt-5.5';
480
514
  case 'mistral':
481
515
  return 'mistral-large-latest';
482
516
  case 'cerebras':
@@ -525,6 +559,8 @@ const createLlmForProvider = (provider, model) => {
525
559
  requireEnv('AZURE_OPENAI_API_KEY');
526
560
  requireEnv('AZURE_OPENAI_ENDPOINT');
527
561
  return new ChatAzure(model);
562
+ case 'codex':
563
+ return new ChatCodex({ model });
528
564
  case 'mistral':
529
565
  return new ChatMistral({
530
566
  model,
@@ -577,7 +613,7 @@ export const getLlmFromCliArgs = (args) => {
577
613
  }
578
614
  const provider = args.provider ?? inferredProvider;
579
615
  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:.`);
616
+ 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
617
  }
582
618
  const normalizedModel = normalizeModelValue(args.model, provider);
583
619
  return createLlmForProvider(provider, normalizedModel);
@@ -778,6 +814,7 @@ export const getCliUsage = () => `Usage:
778
814
  browser-use doctor
779
815
  browser-use install
780
816
  browser-use setup [--mode <local|remote|full>]
817
+ browser-use auth codex <login|status|logout|import>
781
818
  browser-use tunnel <port>
782
819
  browser-use task <list|status|stop|logs>
783
820
  browser-use session <list|get|stop|create|share>
@@ -794,8 +831,8 @@ Options:
794
831
  --mcp Run as MCP server
795
832
  --json Output command results as JSON when supported
796
833
  -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)
834
+ --provider <name> Force provider (openai|anthropic|google|deepseek|groq|openrouter|azure|codex|mistral|cerebras|vercel|oci|ollama|browser-use|aws|aws-anthropic)
835
+ --model <model> Set model (e.g., gpt-5-mini, codex:gpt-5.5, claude-4-sonnet)
799
836
  -p, --prompt <task> Run a single task
800
837
  --mode <name> Setup mode for setup command (local|remote|full)
801
838
  --api-key <value> Browser Use API key for setup or cloud operations
@@ -828,6 +865,141 @@ export const runInstallCommand = (options = {}) => {
828
865
  const writeLine = (stream, value) => {
829
866
  stream.write(`${value}\n`);
830
867
  };
868
+ const formatCodexAuthStatus = (status) => {
869
+ if (!status.authenticated) {
870
+ return [
871
+ 'Codex auth: not authenticated',
872
+ `Auth store: ${status.auth_store_path}`,
873
+ 'Run `browser-use auth codex login` to authenticate.',
874
+ ].join('\n');
875
+ }
876
+ return [
877
+ 'Codex auth: authenticated',
878
+ `Provider: ${status.provider}`,
879
+ `Base URL: ${status.base_url}`,
880
+ `Source: ${status.source ?? 'unknown'}`,
881
+ `Last refresh: ${status.last_refresh ?? 'unknown'}`,
882
+ `Access token expiring: ${String(status.access_token_expiring)}`,
883
+ `Auth store: ${status.auth_store_path}`,
884
+ ].join('\n');
885
+ };
886
+ const parseCodexAuthArgs = (argv) => {
887
+ const flags = {
888
+ json: false,
889
+ force: false,
890
+ importFromCodexCli: false,
891
+ parts: [],
892
+ };
893
+ for (const arg of argv) {
894
+ if (arg === '--json') {
895
+ flags.json = true;
896
+ continue;
897
+ }
898
+ if (arg === '--force') {
899
+ flags.force = true;
900
+ continue;
901
+ }
902
+ if (arg === '--import') {
903
+ flags.importFromCodexCli = true;
904
+ continue;
905
+ }
906
+ if (arg.startsWith('-')) {
907
+ throw new Error(`Unknown auth option: ${arg}`);
908
+ }
909
+ flags.parts.push(arg);
910
+ }
911
+ return flags;
912
+ };
913
+ export const runAuthCommand = async (argv, options = {}) => {
914
+ const output = options.stdout ?? process.stdout;
915
+ const errorOutput = options.stderr ?? process.stderr;
916
+ const loginDeviceCode = options.login_device_code ?? loginAndSaveCodexDeviceCode;
917
+ const importCodexCli = options.import_codex_cli ?? saveImportedCodexCliTokens;
918
+ try {
919
+ const flags = parseCodexAuthArgs(argv);
920
+ const jsonOutput = options.json_output ?? flags.json;
921
+ const provider = flags.parts[0] ?? 'codex';
922
+ const action = flags.parts[1] ?? (flags.importFromCodexCli ? 'import' : 'status');
923
+ if (provider !== 'codex' && provider !== 'openai-codex') {
924
+ throw new Error('Usage: browser-use auth codex <login|status|logout|import>');
925
+ }
926
+ const authOptions = {
927
+ configDir: options.configDir,
928
+ authStorePath: options.authStorePath,
929
+ };
930
+ if (action === 'status') {
931
+ const status = await getCodexAuthStatus(authOptions);
932
+ if (jsonOutput) {
933
+ writeLine(output, JSON.stringify(status, null, 2));
934
+ }
935
+ else {
936
+ writeLine(output, formatCodexAuthStatus(status));
937
+ }
938
+ return status.authenticated ? 0 : 1;
939
+ }
940
+ if (action === 'logout') {
941
+ await clearCodexTokens(authOptions);
942
+ const status = await getCodexAuthStatus(authOptions);
943
+ if (jsonOutput) {
944
+ writeLine(output, JSON.stringify(status, null, 2));
945
+ }
946
+ else {
947
+ writeLine(output, 'Codex auth cleared from browser-use auth store.');
948
+ }
949
+ return 0;
950
+ }
951
+ if (action === 'import' || flags.importFromCodexCli) {
952
+ const imported = await importCodexCli(authOptions);
953
+ if (!imported) {
954
+ writeLine(errorOutput, 'No valid Codex CLI credentials found. Run `browser-use auth codex login` for a separate browser-use session.');
955
+ return 1;
956
+ }
957
+ const status = await getCodexAuthStatus(authOptions);
958
+ if (jsonOutput) {
959
+ writeLine(output, JSON.stringify(status, null, 2));
960
+ }
961
+ else {
962
+ writeLine(output, 'Imported Codex CLI credentials into browser-use auth store.');
963
+ writeLine(output, 'browser-use will refresh its own copy and will not write ~/.codex/auth.json.');
964
+ writeLine(output, 'For the cleanest separation from Codex CLI, prefer `browser-use auth codex login --force`.');
965
+ }
966
+ return 0;
967
+ }
968
+ if (action === 'login') {
969
+ if (!flags.force) {
970
+ const status = await getCodexAuthStatus(authOptions);
971
+ if (status.authenticated) {
972
+ if (jsonOutput) {
973
+ writeLine(output, JSON.stringify(status, null, 2));
974
+ }
975
+ else {
976
+ writeLine(output, 'Existing browser-use Codex credentials found.');
977
+ writeLine(output, 'Use `browser-use auth codex login --force` to create a fresh session.');
978
+ }
979
+ return 0;
980
+ }
981
+ }
982
+ await loginDeviceCode({
983
+ ...authOptions,
984
+ stdout: (jsonOutput ? errorOutput : output),
985
+ });
986
+ const status = await getCodexAuthStatus(authOptions);
987
+ if (jsonOutput) {
988
+ writeLine(output, JSON.stringify(status, null, 2));
989
+ }
990
+ else {
991
+ writeLine(output, 'Codex login successful.');
992
+ writeLine(output, `Auth store: ${status.auth_store_path}`);
993
+ }
994
+ return 0;
995
+ }
996
+ throw new Error('Usage: browser-use auth codex <login|status|logout|import>');
997
+ }
998
+ catch (error) {
999
+ writeLine(errorOutput, `Error: ${error.message}`);
1000
+ return 1;
1001
+ }
1002
+ };
831
1003
  const parseTunnelPort = (value) => {
832
1004
  const port = Number.parseInt(String(value ?? ''), 10);
833
1005
  if (!Number.isFinite(port) || port <= 0) {
@@ -2106,7 +2278,8 @@ export const extractPrefixedSubcommand = (argv) => {
2106
2278
  if (command !== 'run' &&
2107
2279
  command !== 'task' &&
2108
2280
  command !== 'session' &&
2109
- command !== 'profile') {
2281
+ command !== 'profile' &&
2282
+ command !== 'auth') {
2110
2283
  return null;
2111
2284
  }
2112
2285
  return {
@@ -2551,6 +2724,13 @@ export async function main(argv = process.argv.slice(2)) {
2551
2724
  }
2552
2725
  return;
2553
2726
  }
2727
+ if (prefixedSubcommand.command === 'auth') {
2728
+ const exitCode = await runAuthCommand(subcommandArgv);
2729
+ if (exitCode !== 0) {
2730
+ process.exit(exitCode);
2731
+ }
2732
+ return;
2733
+ }
2554
2734
  const exitCode = await runProfileCommand(subcommandArgv);
2555
2735
  if (exitCode !== 0) {
2556
2736
  process.exit(exitCode);
@@ -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
@@ -4,7 +4,7 @@ import path from 'node:path';
4
4
  import { randomUUID } from 'node:crypto';
5
5
  import { config as loadEnv } from 'dotenv';
6
6
  import { createLogger } from './logging-config.js';
7
- loadEnv();
7
+ loadEnv({ quiet: true });
8
8
  const logger = createLogger('browser_use.config');
9
9
  const expand_user = (value) => value.replace(/^~(?=$|\/|\\)/, os.homedir());
10
10
  const resolve_path = (value) => path.resolve(expand_user(value));
@@ -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 = {}) {
@@ -4,6 +4,7 @@ export type ActionHandler = (...args: any[]) => Promise<unknown> | unknown;
4
4
  type BrowserSession = unknown;
5
5
  type BaseChatModel = unknown;
6
6
  type FileSystem = unknown;
7
+ export declare function renderParamsJsonSchema(schema: ZodTypeAny, skipKeys: Set<string>): Record<string, unknown>;
7
8
  export declare class RegisteredAction {
8
9
  readonly name: string;
9
10
  readonly description: string;
@@ -13,6 +14,7 @@ export declare class RegisteredAction {
13
14
  readonly pageFilter: ((page: Page) => boolean) | null;
14
15
  readonly terminates_sequence: boolean;
15
16
  constructor(name: string, description: string, handler: ActionHandler, paramSchema: ZodTypeAny, domains?: string[] | null, pageFilter?: ((page: Page) => boolean) | null, terminates_sequence?: boolean);
17
+ getPromptJsonSchema(): Record<string, unknown>;
16
18
  promptDescription(): string;
17
19
  }
18
20
  export declare class ActionModel {