optexity-browser-use 0.9.5__py3-none-any.whl

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 (147) hide show
  1. browser_use/__init__.py +157 -0
  2. browser_use/actor/__init__.py +11 -0
  3. browser_use/actor/element.py +1175 -0
  4. browser_use/actor/mouse.py +134 -0
  5. browser_use/actor/page.py +561 -0
  6. browser_use/actor/playground/flights.py +41 -0
  7. browser_use/actor/playground/mixed_automation.py +54 -0
  8. browser_use/actor/playground/playground.py +236 -0
  9. browser_use/actor/utils.py +176 -0
  10. browser_use/agent/cloud_events.py +282 -0
  11. browser_use/agent/gif.py +424 -0
  12. browser_use/agent/judge.py +170 -0
  13. browser_use/agent/message_manager/service.py +473 -0
  14. browser_use/agent/message_manager/utils.py +52 -0
  15. browser_use/agent/message_manager/views.py +98 -0
  16. browser_use/agent/prompts.py +413 -0
  17. browser_use/agent/service.py +2316 -0
  18. browser_use/agent/system_prompt.md +185 -0
  19. browser_use/agent/system_prompt_flash.md +10 -0
  20. browser_use/agent/system_prompt_no_thinking.md +183 -0
  21. browser_use/agent/views.py +743 -0
  22. browser_use/browser/__init__.py +41 -0
  23. browser_use/browser/cloud/cloud.py +203 -0
  24. browser_use/browser/cloud/views.py +89 -0
  25. browser_use/browser/events.py +578 -0
  26. browser_use/browser/profile.py +1158 -0
  27. browser_use/browser/python_highlights.py +548 -0
  28. browser_use/browser/session.py +3225 -0
  29. browser_use/browser/session_manager.py +399 -0
  30. browser_use/browser/video_recorder.py +162 -0
  31. browser_use/browser/views.py +200 -0
  32. browser_use/browser/watchdog_base.py +260 -0
  33. browser_use/browser/watchdogs/__init__.py +0 -0
  34. browser_use/browser/watchdogs/aboutblank_watchdog.py +253 -0
  35. browser_use/browser/watchdogs/crash_watchdog.py +335 -0
  36. browser_use/browser/watchdogs/default_action_watchdog.py +2729 -0
  37. browser_use/browser/watchdogs/dom_watchdog.py +817 -0
  38. browser_use/browser/watchdogs/downloads_watchdog.py +1277 -0
  39. browser_use/browser/watchdogs/local_browser_watchdog.py +461 -0
  40. browser_use/browser/watchdogs/permissions_watchdog.py +43 -0
  41. browser_use/browser/watchdogs/popups_watchdog.py +143 -0
  42. browser_use/browser/watchdogs/recording_watchdog.py +126 -0
  43. browser_use/browser/watchdogs/screenshot_watchdog.py +62 -0
  44. browser_use/browser/watchdogs/security_watchdog.py +280 -0
  45. browser_use/browser/watchdogs/storage_state_watchdog.py +335 -0
  46. browser_use/cli.py +2359 -0
  47. browser_use/code_use/__init__.py +16 -0
  48. browser_use/code_use/formatting.py +192 -0
  49. browser_use/code_use/namespace.py +665 -0
  50. browser_use/code_use/notebook_export.py +276 -0
  51. browser_use/code_use/service.py +1340 -0
  52. browser_use/code_use/system_prompt.md +574 -0
  53. browser_use/code_use/utils.py +150 -0
  54. browser_use/code_use/views.py +171 -0
  55. browser_use/config.py +505 -0
  56. browser_use/controller/__init__.py +3 -0
  57. browser_use/dom/enhanced_snapshot.py +161 -0
  58. browser_use/dom/markdown_extractor.py +169 -0
  59. browser_use/dom/playground/extraction.py +312 -0
  60. browser_use/dom/playground/multi_act.py +32 -0
  61. browser_use/dom/serializer/clickable_elements.py +200 -0
  62. browser_use/dom/serializer/code_use_serializer.py +287 -0
  63. browser_use/dom/serializer/eval_serializer.py +478 -0
  64. browser_use/dom/serializer/html_serializer.py +212 -0
  65. browser_use/dom/serializer/paint_order.py +197 -0
  66. browser_use/dom/serializer/serializer.py +1170 -0
  67. browser_use/dom/service.py +825 -0
  68. browser_use/dom/utils.py +129 -0
  69. browser_use/dom/views.py +906 -0
  70. browser_use/exceptions.py +5 -0
  71. browser_use/filesystem/__init__.py +0 -0
  72. browser_use/filesystem/file_system.py +619 -0
  73. browser_use/init_cmd.py +376 -0
  74. browser_use/integrations/gmail/__init__.py +24 -0
  75. browser_use/integrations/gmail/actions.py +115 -0
  76. browser_use/integrations/gmail/service.py +225 -0
  77. browser_use/llm/__init__.py +155 -0
  78. browser_use/llm/anthropic/chat.py +242 -0
  79. browser_use/llm/anthropic/serializer.py +312 -0
  80. browser_use/llm/aws/__init__.py +36 -0
  81. browser_use/llm/aws/chat_anthropic.py +242 -0
  82. browser_use/llm/aws/chat_bedrock.py +289 -0
  83. browser_use/llm/aws/serializer.py +257 -0
  84. browser_use/llm/azure/chat.py +91 -0
  85. browser_use/llm/base.py +57 -0
  86. browser_use/llm/browser_use/__init__.py +3 -0
  87. browser_use/llm/browser_use/chat.py +201 -0
  88. browser_use/llm/cerebras/chat.py +193 -0
  89. browser_use/llm/cerebras/serializer.py +109 -0
  90. browser_use/llm/deepseek/chat.py +212 -0
  91. browser_use/llm/deepseek/serializer.py +109 -0
  92. browser_use/llm/exceptions.py +29 -0
  93. browser_use/llm/google/__init__.py +3 -0
  94. browser_use/llm/google/chat.py +542 -0
  95. browser_use/llm/google/serializer.py +120 -0
  96. browser_use/llm/groq/chat.py +229 -0
  97. browser_use/llm/groq/parser.py +158 -0
  98. browser_use/llm/groq/serializer.py +159 -0
  99. browser_use/llm/messages.py +238 -0
  100. browser_use/llm/models.py +271 -0
  101. browser_use/llm/oci_raw/__init__.py +10 -0
  102. browser_use/llm/oci_raw/chat.py +443 -0
  103. browser_use/llm/oci_raw/serializer.py +229 -0
  104. browser_use/llm/ollama/chat.py +97 -0
  105. browser_use/llm/ollama/serializer.py +143 -0
  106. browser_use/llm/openai/chat.py +264 -0
  107. browser_use/llm/openai/like.py +15 -0
  108. browser_use/llm/openai/serializer.py +165 -0
  109. browser_use/llm/openrouter/chat.py +211 -0
  110. browser_use/llm/openrouter/serializer.py +26 -0
  111. browser_use/llm/schema.py +176 -0
  112. browser_use/llm/views.py +48 -0
  113. browser_use/logging_config.py +330 -0
  114. browser_use/mcp/__init__.py +18 -0
  115. browser_use/mcp/__main__.py +12 -0
  116. browser_use/mcp/client.py +544 -0
  117. browser_use/mcp/controller.py +264 -0
  118. browser_use/mcp/server.py +1114 -0
  119. browser_use/observability.py +204 -0
  120. browser_use/py.typed +0 -0
  121. browser_use/sandbox/__init__.py +41 -0
  122. browser_use/sandbox/sandbox.py +637 -0
  123. browser_use/sandbox/views.py +132 -0
  124. browser_use/screenshots/__init__.py +1 -0
  125. browser_use/screenshots/service.py +52 -0
  126. browser_use/sync/__init__.py +6 -0
  127. browser_use/sync/auth.py +357 -0
  128. browser_use/sync/service.py +161 -0
  129. browser_use/telemetry/__init__.py +51 -0
  130. browser_use/telemetry/service.py +112 -0
  131. browser_use/telemetry/views.py +101 -0
  132. browser_use/tokens/__init__.py +0 -0
  133. browser_use/tokens/custom_pricing.py +24 -0
  134. browser_use/tokens/mappings.py +4 -0
  135. browser_use/tokens/service.py +580 -0
  136. browser_use/tokens/views.py +108 -0
  137. browser_use/tools/registry/service.py +572 -0
  138. browser_use/tools/registry/views.py +174 -0
  139. browser_use/tools/service.py +1675 -0
  140. browser_use/tools/utils.py +82 -0
  141. browser_use/tools/views.py +100 -0
  142. browser_use/utils.py +670 -0
  143. optexity_browser_use-0.9.5.dist-info/METADATA +344 -0
  144. optexity_browser_use-0.9.5.dist-info/RECORD +147 -0
  145. optexity_browser_use-0.9.5.dist-info/WHEEL +4 -0
  146. optexity_browser_use-0.9.5.dist-info/entry_points.txt +3 -0
  147. optexity_browser_use-0.9.5.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,132 @@
1
+ """Type-safe event models for sandbox execution SSE streaming"""
2
+
3
+ import json
4
+ from enum import Enum
5
+ from typing import Any
6
+
7
+ from pydantic import BaseModel
8
+
9
+
10
+ class SandboxError(Exception):
11
+ pass
12
+
13
+
14
+ class SSEEventType(str, Enum):
15
+ """Event types for Server-Sent Events"""
16
+
17
+ BROWSER_CREATED = 'browser_created'
18
+ INSTANCE_CREATED = 'instance_created'
19
+ INSTANCE_READY = 'instance_ready'
20
+ LOG = 'log'
21
+ RESULT = 'result'
22
+ ERROR = 'error'
23
+ STREAM_COMPLETE = 'stream_complete'
24
+
25
+
26
+ class BrowserCreatedData(BaseModel):
27
+ """Data for browser_created event"""
28
+
29
+ session_id: str
30
+ live_url: str
31
+ status: str
32
+
33
+
34
+ class LogData(BaseModel):
35
+ """Data for log event"""
36
+
37
+ message: str
38
+ level: str = 'info' # stdout, stderr, info, warning, error
39
+
40
+
41
+ class ExecutionResponse(BaseModel):
42
+ """Execution result from the executor"""
43
+
44
+ success: bool
45
+ result: Any = None
46
+ error: str | None = None
47
+ traceback: str | None = None
48
+
49
+
50
+ class ResultData(BaseModel):
51
+ """Data for result event"""
52
+
53
+ execution_response: ExecutionResponse
54
+
55
+
56
+ class ErrorData(BaseModel):
57
+ """Data for error event"""
58
+
59
+ error: str
60
+ traceback: str | None = None
61
+ status_code: int = 500
62
+
63
+
64
+ class SSEEvent(BaseModel):
65
+ """Type-safe SSE Event
66
+
67
+ Usage:
68
+ # Parse from JSON
69
+ event = SSEEvent.from_json(event_json_string)
70
+
71
+ # Type-safe access with type guards
72
+ if event.is_browser_created():
73
+ assert isinstance(event.data, BrowserCreatedData)
74
+ print(event.data.live_url)
75
+
76
+ # Or check event type directly
77
+ if event.type == SSEEventType.LOG:
78
+ assert isinstance(event.data, LogData)
79
+ print(event.data.message)
80
+ """
81
+
82
+ type: SSEEventType
83
+ data: BrowserCreatedData | LogData | ResultData | ErrorData | dict[str, Any]
84
+ timestamp: str | None = None
85
+
86
+ @classmethod
87
+ def from_json(cls, event_json: str) -> 'SSEEvent':
88
+ """Parse SSE event from JSON string with proper type discrimination
89
+
90
+ Args:
91
+ event_json: JSON string from SSE stream
92
+
93
+ Returns:
94
+ Typed SSEEvent with appropriate data model
95
+
96
+ Raises:
97
+ json.JSONDecodeError: If JSON is malformed
98
+ ValueError: If event type is invalid
99
+ """
100
+ raw_data = json.loads(event_json)
101
+ event_type = SSEEventType(raw_data.get('type'))
102
+ data_dict = raw_data.get('data', {})
103
+
104
+ # Parse data based on event type
105
+ if event_type == SSEEventType.BROWSER_CREATED:
106
+ data = BrowserCreatedData(**data_dict)
107
+ elif event_type == SSEEventType.LOG:
108
+ data = LogData(**data_dict)
109
+ elif event_type == SSEEventType.RESULT:
110
+ data = ResultData(**data_dict)
111
+ elif event_type == SSEEventType.ERROR:
112
+ data = ErrorData(**data_dict)
113
+ else:
114
+ data = data_dict
115
+
116
+ return cls(type=event_type, data=data, timestamp=raw_data.get('timestamp'))
117
+
118
+ def is_browser_created(self) -> bool:
119
+ """Type guard for BrowserCreatedData"""
120
+ return self.type == SSEEventType.BROWSER_CREATED and isinstance(self.data, BrowserCreatedData)
121
+
122
+ def is_log(self) -> bool:
123
+ """Type guard for LogData"""
124
+ return self.type == SSEEventType.LOG and isinstance(self.data, LogData)
125
+
126
+ def is_result(self) -> bool:
127
+ """Type guard for ResultData"""
128
+ return self.type == SSEEventType.RESULT and isinstance(self.data, ResultData)
129
+
130
+ def is_error(self) -> bool:
131
+ """Type guard for ErrorData"""
132
+ return self.type == SSEEventType.ERROR and isinstance(self.data, ErrorData)
@@ -0,0 +1 @@
1
+ # Screenshots package for browser-use
@@ -0,0 +1,52 @@
1
+ """
2
+ Screenshot storage service for browser-use agents.
3
+ """
4
+
5
+ import base64
6
+ from pathlib import Path
7
+
8
+ import anyio
9
+
10
+ from browser_use.observability import observe_debug
11
+
12
+
13
+ class ScreenshotService:
14
+ """Simple screenshot storage service that saves screenshots to disk"""
15
+
16
+ def __init__(self, agent_directory: str | Path):
17
+ """Initialize with agent directory path"""
18
+ self.agent_directory = Path(agent_directory) if isinstance(agent_directory, str) else agent_directory
19
+
20
+ # Create screenshots subdirectory
21
+ self.screenshots_dir = self.agent_directory / 'screenshots'
22
+ self.screenshots_dir.mkdir(parents=True, exist_ok=True)
23
+
24
+ @observe_debug(ignore_input=True, ignore_output=True, name='store_screenshot')
25
+ async def store_screenshot(self, screenshot_b64: str, step_number: int) -> str:
26
+ """Store screenshot to disk and return the full path as string"""
27
+ screenshot_filename = f'step_{step_number}.png'
28
+ screenshot_path = self.screenshots_dir / screenshot_filename
29
+
30
+ # Decode base64 and save to disk
31
+ screenshot_data = base64.b64decode(screenshot_b64)
32
+
33
+ async with await anyio.open_file(screenshot_path, 'wb') as f:
34
+ await f.write(screenshot_data)
35
+
36
+ return str(screenshot_path)
37
+
38
+ @observe_debug(ignore_input=True, ignore_output=True, name='get_screenshot_from_disk')
39
+ async def get_screenshot(self, screenshot_path: str) -> str | None:
40
+ """Load screenshot from disk path and return as base64"""
41
+ if not screenshot_path:
42
+ return None
43
+
44
+ path = Path(screenshot_path)
45
+ if not path.exists():
46
+ return None
47
+
48
+ # Load from disk and encode to base64
49
+ async with await anyio.open_file(path, 'rb') as f:
50
+ screenshot_data = await f.read()
51
+
52
+ return base64.b64encode(screenshot_data).decode('utf-8')
@@ -0,0 +1,6 @@
1
+ """Cloud sync module for Browser Use."""
2
+
3
+ from browser_use.sync.auth import CloudAuthConfig, DeviceAuthClient
4
+ from browser_use.sync.service import CloudSync
5
+
6
+ __all__ = ['CloudAuthConfig', 'DeviceAuthClient', 'CloudSync']
@@ -0,0 +1,357 @@
1
+ """
2
+ OAuth2 Device Authorization Grant flow client for browser-use.
3
+ """
4
+
5
+ import asyncio
6
+ import json
7
+ import os
8
+ import shutil
9
+ import time
10
+ from datetime import datetime
11
+
12
+ import httpx
13
+ from pydantic import BaseModel
14
+ from uuid_extensions import uuid7str
15
+
16
+ from browser_use.config import CONFIG
17
+
18
+ # Temporary user ID for pre-auth events (matches cloud backend)
19
+ TEMP_USER_ID = '99999999-9999-9999-9999-999999999999'
20
+
21
+
22
+ def get_or_create_device_id() -> str:
23
+ """Get or create a persistent device ID for this installation."""
24
+ device_id_path = CONFIG.BROWSER_USE_CONFIG_DIR / 'device_id'
25
+
26
+ # Try to read existing device ID
27
+ if device_id_path.exists():
28
+ try:
29
+ device_id = device_id_path.read_text().strip()
30
+ if device_id: # Make sure it's not empty
31
+ return device_id
32
+ except Exception:
33
+ # If we can't read it, we'll create a new one
34
+ pass
35
+
36
+ # Create new device ID
37
+ device_id = uuid7str()
38
+
39
+ # Ensure config directory exists
40
+ CONFIG.BROWSER_USE_CONFIG_DIR.mkdir(parents=True, exist_ok=True)
41
+
42
+ # Write device ID to file
43
+ device_id_path.write_text(device_id)
44
+
45
+ return device_id
46
+
47
+
48
+ class CloudAuthConfig(BaseModel):
49
+ """Configuration for cloud authentication"""
50
+
51
+ api_token: str | None = None
52
+ user_id: str | None = None
53
+ authorized_at: datetime | None = None
54
+
55
+ @classmethod
56
+ def load_from_file(cls) -> 'CloudAuthConfig':
57
+ """Load auth config from local file"""
58
+
59
+ config_path = CONFIG.BROWSER_USE_CONFIG_DIR / 'cloud_auth.json'
60
+ if config_path.exists():
61
+ try:
62
+ with open(config_path) as f:
63
+ data = json.load(f)
64
+ return cls.model_validate(data)
65
+ except Exception:
66
+ # Return empty config if file is corrupted
67
+ pass
68
+ return cls()
69
+
70
+ def save_to_file(self) -> None:
71
+ """Save auth config to local file"""
72
+
73
+ CONFIG.BROWSER_USE_CONFIG_DIR.mkdir(parents=True, exist_ok=True)
74
+
75
+ config_path = CONFIG.BROWSER_USE_CONFIG_DIR / 'cloud_auth.json'
76
+ with open(config_path, 'w') as f:
77
+ json.dump(self.model_dump(mode='json'), f, indent=2, default=str)
78
+
79
+ # Set restrictive permissions (owner read/write only) for security
80
+ try:
81
+ os.chmod(config_path, 0o600)
82
+ except Exception:
83
+ # Some systems may not support chmod, continue anyway
84
+ pass
85
+
86
+
87
+ class DeviceAuthClient:
88
+ """Client for OAuth2 device authorization flow"""
89
+
90
+ def __init__(self, base_url: str | None = None, http_client: httpx.AsyncClient | None = None):
91
+ # Backend API URL for OAuth requests - can be passed directly or defaults to env var
92
+ self.base_url = base_url or CONFIG.BROWSER_USE_CLOUD_API_URL
93
+ self.client_id = 'library'
94
+ self.scope = 'read write'
95
+
96
+ # If no client provided, we'll create one per request
97
+ self.http_client = http_client
98
+
99
+ # Temporary user ID for pre-auth events
100
+ self.temp_user_id = TEMP_USER_ID
101
+
102
+ # Get or create persistent device ID
103
+ self.device_id = get_or_create_device_id()
104
+
105
+ # Load existing auth if available
106
+ self.auth_config = CloudAuthConfig.load_from_file()
107
+
108
+ @property
109
+ def is_authenticated(self) -> bool:
110
+ """Check if we have valid authentication"""
111
+ return bool(self.auth_config.api_token and self.auth_config.user_id)
112
+
113
+ @property
114
+ def api_token(self) -> str | None:
115
+ """Get the current API token"""
116
+ return self.auth_config.api_token
117
+
118
+ @property
119
+ def user_id(self) -> str:
120
+ """Get the current user ID (temporary or real)"""
121
+ return self.auth_config.user_id or self.temp_user_id
122
+
123
+ async def start_device_authorization(
124
+ self,
125
+ agent_session_id: str | None = None,
126
+ ) -> dict:
127
+ """
128
+ Start the device authorization flow.
129
+ Returns device authorization details including user code and verification URL.
130
+ """
131
+ if self.http_client:
132
+ response = await self.http_client.post(
133
+ f'{self.base_url.rstrip("/")}/api/v1/oauth/device/authorize',
134
+ data={
135
+ 'client_id': self.client_id,
136
+ 'scope': self.scope,
137
+ 'agent_session_id': agent_session_id or '',
138
+ 'device_id': self.device_id,
139
+ },
140
+ )
141
+ response.raise_for_status()
142
+ return response.json()
143
+ else:
144
+ async with httpx.AsyncClient() as client:
145
+ response = await client.post(
146
+ f'{self.base_url.rstrip("/")}/api/v1/oauth/device/authorize',
147
+ data={
148
+ 'client_id': self.client_id,
149
+ 'scope': self.scope,
150
+ 'agent_session_id': agent_session_id or '',
151
+ 'device_id': self.device_id,
152
+ },
153
+ )
154
+ response.raise_for_status()
155
+ return response.json()
156
+
157
+ async def poll_for_token(
158
+ self,
159
+ device_code: str,
160
+ interval: float = 3.0,
161
+ timeout: float = 1800.0,
162
+ ) -> dict | None:
163
+ """
164
+ Poll for the access token.
165
+ Returns token info when authorized, None if timeout.
166
+ """
167
+ start_time = time.time()
168
+
169
+ if self.http_client:
170
+ # Use injected client for all requests
171
+ while time.time() - start_time < timeout:
172
+ try:
173
+ response = await self.http_client.post(
174
+ f'{self.base_url.rstrip("/")}/api/v1/oauth/device/token',
175
+ data={
176
+ 'grant_type': 'urn:ietf:params:oauth:grant-type:device_code',
177
+ 'device_code': device_code,
178
+ 'client_id': self.client_id,
179
+ },
180
+ )
181
+
182
+ if response.status_code == 200:
183
+ data = response.json()
184
+
185
+ # Check for pending authorization
186
+ if data.get('error') == 'authorization_pending':
187
+ await asyncio.sleep(interval)
188
+ continue
189
+
190
+ # Check for slow down
191
+ if data.get('error') == 'slow_down':
192
+ interval = data.get('interval', interval * 2)
193
+ await asyncio.sleep(interval)
194
+ continue
195
+
196
+ # Check for other errors
197
+ if 'error' in data:
198
+ print(f'Error: {data.get("error_description", data["error"])}')
199
+ return None
200
+
201
+ # Success! We have a token
202
+ if 'access_token' in data:
203
+ return data
204
+
205
+ elif response.status_code == 400:
206
+ # Error response
207
+ data = response.json()
208
+ if data.get('error') not in ['authorization_pending', 'slow_down']:
209
+ print(f'Error: {data.get("error_description", "Unknown error")}')
210
+ return None
211
+
212
+ else:
213
+ print(f'Unexpected status code: {response.status_code}')
214
+ return None
215
+
216
+ except Exception as e:
217
+ print(f'Error polling for token: {e}')
218
+
219
+ await asyncio.sleep(interval)
220
+ else:
221
+ # Create a new client for polling
222
+ async with httpx.AsyncClient() as client:
223
+ while time.time() - start_time < timeout:
224
+ try:
225
+ response = await client.post(
226
+ f'{self.base_url.rstrip("/")}/api/v1/oauth/device/token',
227
+ data={
228
+ 'grant_type': 'urn:ietf:params:oauth:grant-type:device_code',
229
+ 'device_code': device_code,
230
+ 'client_id': self.client_id,
231
+ },
232
+ )
233
+
234
+ if response.status_code == 200:
235
+ data = response.json()
236
+
237
+ # Check for pending authorization
238
+ if data.get('error') == 'authorization_pending':
239
+ await asyncio.sleep(interval)
240
+ continue
241
+
242
+ # Check for slow down
243
+ if data.get('error') == 'slow_down':
244
+ interval = data.get('interval', interval * 2)
245
+ await asyncio.sleep(interval)
246
+ continue
247
+
248
+ # Check for other errors
249
+ if 'error' in data:
250
+ print(f'Error: {data.get("error_description", data["error"])}')
251
+ return None
252
+
253
+ # Success! We have a token
254
+ if 'access_token' in data:
255
+ return data
256
+
257
+ elif response.status_code == 400:
258
+ # Error response
259
+ data = response.json()
260
+ if data.get('error') not in ['authorization_pending', 'slow_down']:
261
+ print(f'Error: {data.get("error_description", "Unknown error")}')
262
+ return None
263
+
264
+ else:
265
+ print(f'Unexpected status code: {response.status_code}')
266
+ return None
267
+
268
+ except Exception as e:
269
+ print(f'Error polling for token: {e}')
270
+
271
+ await asyncio.sleep(interval)
272
+
273
+ return None
274
+
275
+ async def authenticate(
276
+ self,
277
+ agent_session_id: str | None = None,
278
+ show_instructions: bool = True,
279
+ ) -> bool:
280
+ """
281
+ Run the full authentication flow.
282
+ Returns True if authentication successful.
283
+ """
284
+ import logging
285
+
286
+ logger = logging.getLogger(__name__)
287
+
288
+ try:
289
+ # Start device authorization
290
+ device_auth = await self.start_device_authorization(agent_session_id)
291
+
292
+ # Use frontend URL for user-facing links
293
+ frontend_url = CONFIG.BROWSER_USE_CLOUD_UI_URL or self.base_url.replace('//api.', '//cloud.')
294
+
295
+ # Replace backend URL with frontend URL in verification URIs
296
+ verification_uri = device_auth['verification_uri'].replace(self.base_url, frontend_url)
297
+ verification_uri_complete = device_auth['verification_uri_complete'].replace(self.base_url, frontend_url)
298
+
299
+ terminal_width, _terminal_height = shutil.get_terminal_size((80, 20))
300
+ if show_instructions and CONFIG.BROWSER_USE_CLOUD_SYNC:
301
+ logger.info('─' * max(terminal_width - 40, 20))
302
+ logger.info('🌐 View the details of this run in Browser Use Cloud:')
303
+ logger.info(f' 👉 {verification_uri_complete}')
304
+ logger.info('─' * max(terminal_width - 40, 20) + '\n')
305
+
306
+ # Poll for token
307
+ token_data = await self.poll_for_token(
308
+ device_code=device_auth['device_code'],
309
+ interval=device_auth.get('interval', 5),
310
+ )
311
+
312
+ if token_data and token_data.get('access_token'):
313
+ # Save authentication
314
+ self.auth_config.api_token = token_data['access_token']
315
+ self.auth_config.user_id = token_data.get('user_id', self.temp_user_id)
316
+ self.auth_config.authorized_at = datetime.now()
317
+ self.auth_config.save_to_file()
318
+
319
+ if show_instructions:
320
+ logger.debug('✅ Authentication successful! Cloud sync is now enabled with your browser-use account.')
321
+
322
+ return True
323
+
324
+ except httpx.HTTPStatusError as e:
325
+ # HTTP error with response
326
+ if e.response.status_code == 404:
327
+ logger.warning(
328
+ 'Cloud sync authentication endpoint not found (404). Check your BROWSER_USE_CLOUD_API_URL setting.'
329
+ )
330
+ else:
331
+ logger.warning(f'Failed to authenticate with cloud service: HTTP {e.response.status_code} - {e.response.text}')
332
+ except httpx.RequestError as e:
333
+ # Connection/network errors
334
+ # logger.warning(f'Failed to connect to cloud service: {type(e).__name__}: {e}')
335
+ pass
336
+ except Exception as e:
337
+ # Other unexpected errors
338
+ logger.warning(f'❌ Unexpected error during cloud sync authentication: {type(e).__name__}: {e}')
339
+
340
+ if show_instructions:
341
+ logger.debug(f'❌ Sync authentication failed or timed out with {CONFIG.BROWSER_USE_CLOUD_API_URL}')
342
+
343
+ return False
344
+
345
+ def get_headers(self) -> dict:
346
+ """Get headers for API requests"""
347
+ if self.api_token:
348
+ return {'Authorization': f'Bearer {self.api_token}'}
349
+ return {}
350
+
351
+ def clear_auth(self) -> None:
352
+ """Clear stored authentication"""
353
+ self.auth_config = CloudAuthConfig()
354
+
355
+ # Remove the config file entirely instead of saving empty values
356
+ config_path = CONFIG.BROWSER_USE_CONFIG_DIR / 'cloud_auth.json'
357
+ config_path.unlink(missing_ok=True)