Merge branch 'main' into codex/ci-pin-stale-bot-sha

This commit is contained in:
grtninja
2026-04-11 22:15:59 -04:00
committed by GitHub
6 changed files with 58 additions and 52 deletions

View File

@@ -25,7 +25,12 @@ from browser_use.llm.messages import (
UserMessage,
)
from browser_use.observability import observe_debug
from browser_use.utils import match_url_with_domain_pattern, time_execution_sync
from browser_use.utils import (
collect_sensitive_data_values,
match_url_with_domain_pattern,
redact_sensitive_string,
time_execution_sync,
)
logger = logging.getLogger(__name__)
@@ -573,30 +578,14 @@ class MessageManager:
if not self.sensitive_data:
return value
# Collect all sensitive values, immediately converting old format to new format
sensitive_values: dict[str, str] = {}
# Process all sensitive data entries
for key_or_domain, content in self.sensitive_data.items():
if isinstance(content, dict):
# Already in new format: {domain: {key: value}}
for key, val in content.items():
if val: # Skip empty values
sensitive_values[key] = val
elif content: # Old format: {key: value} - convert to new format internally
# We treat this as if it was {'http*://*': {key_or_domain: content}}
sensitive_values[key_or_domain] = content
sensitive_values = collect_sensitive_data_values(self.sensitive_data)
# If there are no valid sensitive data entries, just return the original value
if not sensitive_values:
logger.warning('No valid entries found in sensitive_data dictionary')
return value
# Replace all valid sensitive data values with their placeholder tags
for key, val in sensitive_values.items():
value = value.replace(val, f'<secret>{key}</secret>')
return value
return redact_sensitive_string(value, sensitive_values)
if isinstance(message.content, str):
message.content = replace_sensitive(message.content)

View File

@@ -27,6 +27,7 @@ from browser_use.filesystem.file_system import FileSystemState
from browser_use.llm.base import BaseChatModel
from browser_use.tokens.views import UsageSummary
from browser_use.tools.registry.views import ActionModel
from browser_use.utils import collect_sensitive_data_values, redact_sensitive_string
logger = logging.getLogger(__name__)
@@ -512,29 +513,13 @@ class AgentHistory(BaseModel):
if not sensitive_data:
return value
# Collect all sensitive values, immediately converting old format to new format
sensitive_values: dict[str, str] = {}
# Process all sensitive data entries
for key_or_domain, content in sensitive_data.items():
if isinstance(content, dict):
# Already in new format: {domain: {key: value}}
for key, val in content.items():
if val: # Skip empty values
sensitive_values[key] = val
elif content: # Old format: {key: value} - convert to new format internally
# We treat this as if it was {'http*://*': {key_or_domain: content}}
sensitive_values[key_or_domain] = content
sensitive_values = collect_sensitive_data_values(sensitive_data)
# If there are no valid sensitive data entries, just return the original value
if not sensitive_values:
return value
# Replace all valid sensitive data values with their placeholder tags
for key, val in sensitive_values.items():
value = value.replace(val, f'<secret>{key}</secret>')
return value
return redact_sensitive_string(value, sensitive_values)
def _filter_sensitive_data_from_dict(
self, data: dict[str, Any], sensitive_data: dict[str, str | dict[str, str]] | None

View File

@@ -518,6 +518,11 @@ class DefaultActionWatchdog(BaseWatchdog):
raise BrowserError(error_msg)
try:
def invalidate_dom_cache() -> None:
if self.browser_session._dom_watchdog:
self.browser_session._dom_watchdog.clear_cache()
# Convert direction and amount to pixels
# Positive pixels = scroll down, negative = scroll up
pixels = event.amount if event.direction == 'down' else -event.amount
@@ -547,6 +552,7 @@ class DefaultActionWatchdog(BaseWatchdog):
# Wait a bit for the scroll to settle and DOM to update
await asyncio.sleep(0.2)
invalidate_dom_cache()
return None
# Perform target-level scroll
@@ -554,6 +560,7 @@ class DefaultActionWatchdog(BaseWatchdog):
# Note: We don't clear cached state here - let multi_act handle DOM change detection
# by explicitly rebuilding and comparing when needed
invalidate_dom_cache()
# Log success
self.logger.debug(f'📜 Scrolled {event.direction} by {event.amount} pixels')

View File

@@ -1104,10 +1104,12 @@ class DomService:
pagination_buttons: list[dict[str, str | int | bool]] = []
# Common pagination patterns to look for
# `«` and `»` are ambiguous across sites, so treat them only as prev/next
# fallback symbols and let word-based first/last signals win
next_patterns = ['next', '>', '»', '', 'siguiente', 'suivant', 'weiter', 'volgende']
prev_patterns = ['prev', 'previous', '<', '«', '', 'anterior', 'précédent', 'zurück', 'vorige']
first_patterns = ['first', '', '«', 'primera', 'première', 'erste', 'eerste']
last_patterns = ['last', '', '»', 'última', 'dernier', 'letzte', 'laatste']
first_patterns = ['first', '', 'primera', 'première', 'erste', 'eerste']
last_patterns = ['last', '', 'última', 'dernier', 'letzte', 'laatste']
for index, node in selector_map.items():
# Skip non-clickable elements
@@ -1133,18 +1135,18 @@ class DomService:
button_type: str | None = None
# Check for next button
if any(pattern in all_text for pattern in next_patterns):
button_type = 'next'
# Check for previous button
elif any(pattern in all_text for pattern in prev_patterns):
button_type = 'prev'
# Check for first button
elif any(pattern in all_text for pattern in first_patterns):
# Match specific first/last semantics before generic prev/next fallbacks.
if any(pattern in all_text for pattern in first_patterns):
button_type = 'first'
# Check for last button
elif any(pattern in all_text for pattern in last_patterns):
button_type = 'last'
# Check for next button
elif any(pattern in all_text for pattern in next_patterns):
button_type = 'next'
# Check for previous button
elif any(pattern in all_text for pattern in prev_patterns):
button_type = 'prev'
# Check for numeric page buttons (single or double digit)
elif text.isdigit() and len(text) <= 2 and role in ['button', 'link', '']:
button_type = 'page_number'

View File

@@ -1222,8 +1222,7 @@ def main() -> int:
if args.command == 'doctor':
from browser_use.skill_cli.commands import doctor
loop = asyncio.get_event_loop()
result = loop.run_until_complete(doctor.handle())
result = asyncio.run(doctor.handle())
if args.json:
print(json.dumps(result))
@@ -1337,9 +1336,9 @@ def main() -> int:
port_arg = getattr(args, 'port_arg', None)
if getattr(args, 'all', False):
# stop --all
result = asyncio.get_event_loop().run_until_complete(tunnel.stop_all_tunnels())
result = asyncio.run(tunnel.stop_all_tunnels())
elif port_arg is not None:
result = asyncio.get_event_loop().run_until_complete(tunnel.stop_tunnel(port_arg))
result = asyncio.run(tunnel.stop_tunnel(port_arg))
else:
print('Usage: browser-use tunnel stop <port> | --all', file=sys.stderr)
return 1
@@ -1349,7 +1348,7 @@ def main() -> int:
except ValueError:
print(f'Unknown tunnel subcommand: {pos}', file=sys.stderr)
return 1
result = asyncio.get_event_loop().run_until_complete(tunnel.start_tunnel(port))
result = asyncio.run(tunnel.start_tunnel(port))
else:
print('Usage: browser-use tunnel <port> | list | stop <port>', file=sys.stderr)
return 0

View File

@@ -31,6 +31,30 @@ _openai_bad_request_error: type | None = None
_groq_bad_request_error: type | None = None
def collect_sensitive_data_values(sensitive_data: dict[str, str | dict[str, str]] | None) -> dict[str, str]:
"""Flatten legacy and domain-scoped sensitive data into placeholder -> value mappings."""
if not sensitive_data:
return {}
sensitive_values: dict[str, str] = {}
for key_or_domain, content in sensitive_data.items():
if isinstance(content, dict):
for key, val in content.items():
if val:
sensitive_values[key] = val
elif content:
sensitive_values[key_or_domain] = content
return sensitive_values
def redact_sensitive_string(value: str, sensitive_values: dict[str, str]) -> str:
"""Replace sensitive values with placeholders, longest matches first to avoid partial leaks."""
for key, secret in sorted(sensitive_values.items(), key=lambda item: len(item[1]), reverse=True):
value = value.replace(secret, f'<secret>{key}</secret>')
return value
def _get_openai_bad_request_error() -> type | None:
"""Lazy loader for OpenAI BadRequestError."""
global _openai_bad_request_error