browser-use 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -18
- package/dist/actor/element.js +24 -3
- package/dist/actor/mouse.js +21 -3
- package/dist/actor/page.js +33 -11
- package/dist/agent/gif.js +28 -3
- package/dist/agent/message-manager/service.js +2 -22
- package/dist/agent/message-manager/utils.js +15 -2
- package/dist/agent/message-manager/views.d.ts +7 -7
- package/dist/agent/message-manager/views.js +1 -0
- package/dist/agent/prompts.d.ts +3 -0
- package/dist/agent/prompts.js +22 -12
- package/dist/agent/service.d.ts +9 -1
- package/dist/agent/service.js +215 -81
- package/dist/agent/system_prompt.md +12 -11
- package/dist/agent/system_prompt_anthropic_flash.md +6 -5
- package/dist/agent/system_prompt_no_thinking.md +12 -11
- package/dist/agent/views.d.ts +2 -0
- package/dist/agent/views.js +48 -36
- package/dist/browser/extensions.js +20 -10
- package/dist/browser/profile.d.ts +4 -0
- package/dist/browser/profile.js +107 -4
- package/dist/browser/session.d.ts +28 -1
- package/dist/browser/session.js +1436 -528
- package/dist/browser/watchdogs/default-action-watchdog.js +32 -3
- package/dist/browser/watchdogs/downloads-watchdog.d.ts +4 -0
- package/dist/browser/watchdogs/downloads-watchdog.js +105 -9
- package/dist/browser/watchdogs/har-recording-watchdog.d.ts +1 -0
- package/dist/browser/watchdogs/har-recording-watchdog.js +54 -2
- package/dist/browser/watchdogs/permissions-watchdog.d.ts +5 -0
- package/dist/browser/watchdogs/permissions-watchdog.js +106 -3
- package/dist/browser/watchdogs/recording-watchdog.d.ts +2 -0
- package/dist/browser/watchdogs/recording-watchdog.js +54 -2
- package/dist/browser/watchdogs/security-watchdog.d.ts +1 -0
- package/dist/browser/watchdogs/security-watchdog.js +47 -7
- package/dist/browser/watchdogs/storage-state-watchdog.d.ts +6 -0
- package/dist/browser/watchdogs/storage-state-watchdog.js +206 -14
- package/dist/cli.d.ts +13 -2
- package/dist/cli.js +188 -8
- package/dist/code-use/namespace.js +52 -7
- package/dist/code-use/notebook-export.js +18 -2
- package/dist/code-use/service.js +1 -0
- package/dist/config.js +27 -5
- package/dist/controller/action-timeout.d.ts +9 -0
- package/dist/controller/action-timeout.js +95 -0
- package/dist/controller/registry/service.d.ts +1 -0
- package/dist/controller/registry/service.js +28 -1
- package/dist/controller/registry/views.d.ts +2 -0
- package/dist/controller/registry/views.js +44 -17
- package/dist/controller/service.d.ts +2 -1
- package/dist/controller/service.js +494 -329
- package/dist/filesystem/file-system.js +38 -8
- package/dist/integrations/gmail/service.js +30 -6
- package/dist/llm/browser-use/chat.js +2 -2
- package/dist/llm/codex/auth.d.ts +118 -0
- package/dist/llm/codex/auth.js +599 -0
- package/dist/llm/codex/chat.d.ts +70 -0
- package/dist/llm/codex/chat.js +392 -0
- package/dist/llm/codex/index.d.ts +2 -0
- package/dist/llm/codex/index.js +2 -0
- package/dist/llm/google/chat.js +18 -1
- package/dist/logging-config.js +22 -11
- package/dist/mcp/client.d.ts +1 -0
- package/dist/mcp/client.js +12 -10
- package/dist/mcp/redaction.d.ts +3 -0
- package/dist/mcp/redaction.js +132 -0
- package/dist/mcp/server.d.ts +2 -0
- package/dist/mcp/server.js +64 -22
- package/dist/observability.js +1 -1
- package/dist/screenshots/service.js +25 -2
- package/dist/skill-cli/direct.d.ts +4 -1
- package/dist/skill-cli/direct.js +260 -64
- package/dist/skill-cli/server.d.ts +1 -0
- package/dist/skill-cli/server.js +115 -25
- package/dist/skill-cli/tunnel.d.ts +1 -0
- package/dist/skill-cli/tunnel.js +16 -4
- package/dist/sync/auth.js +22 -9
- package/dist/telemetry/service.js +21 -2
- package/dist/telemetry/views.js +31 -8
- package/dist/tokens/custom-pricing.js +2 -2
- package/dist/tokens/openrouter-pricing.d.ts +11 -0
- package/dist/tokens/openrouter-pricing.js +102 -0
- package/dist/tokens/service.js +20 -16
- package/dist/utils.d.ts +3 -1
- package/dist/utils.js +4 -2
- package/package.json +75 -33
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
|
|
324
|
-
await fs.writeFile(historyPath, JSON.stringify(normalized, null, 2),
|
|
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
|
|
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
|
-
|
|
62
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
-
|
|
41
|
-
fs.writeFileSync(resolved, JSON.stringify(notebook, null, 2),
|
|
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) => {
|
package/dist/code-use/service.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|