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.
- package/README.md +24 -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 +204 -79
- 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 +190 -9
- 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 +26 -4
- 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/service.d.ts +2 -1
- package/dist/controller/service.js +494 -329
- package/dist/entrypoint.d.ts +1 -0
- package/dist/entrypoint.js +27 -0
- 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/screenshots/service.js +25 -2
- package/dist/skill-cli/direct.d.ts +4 -1
- package/dist/skill-cli/direct.js +263 -66
- 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 +3 -1
- 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
|
|
324
|
-
await fs.writeFile(historyPath, JSON.stringify(normalized, null, 2),
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
@@ -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 = {}) {
|
|
@@ -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 {};
|