Skip to content

Commit 4e67962

Browse files
guyernestclaude
andcommitted
feat(browser-agent): Centralize browser launch config for password manager support
New browser_launch_config.py module: - Centralized configuration for all browser launch arguments - get_launch_options() returns consistent options across all entry points - Conditional --no-sandbox (only in containerized environments) - Environment detection via is_containerized_environment() - Support for BROWSER_LAUNCH_ARGS env var for custom args Key browser argument changes: - REMOVE --enable-automation: Hides "controlled by automation" bar, enables password save prompts and autofill - REMOVE --disable-component-extensions-with-background-pages: Enables password manager popup UI (critical for built-in and third-party managers) - ADD --disable-blink-features=AutomationControlled: Only for ephemeral sessions (hides navigator.webdriver flag) - ADD --no-sandbox: Only in Docker/container environments (security) Fixed computer_agent_wrapper.py: - Profile resolution now uses resolve_profile() for tag-based fallback - If profile_name not found, falls back to matching required_tags - setup_login now uses shared launch config for password manager support Updated openai_playwright_executor.py: - _init_browser() now uses shared get_launch_options() - Consistent arguments with profile setup and other entry points - Logs launch configuration for debugging 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 10fa90e commit 4e67962

3 files changed

Lines changed: 330 additions & 55 deletions

File tree

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Browser Launch Configuration
4+
5+
Centralized configuration for Playwright browser launch arguments.
6+
Ensures consistent browser behavior across all entry points:
7+
- Profile setup (setup_login)
8+
- Script execution (openai_playwright_executor, computer_agent_wrapper)
9+
- Testing
10+
11+
Key Goals:
12+
1. Enable password manager functionality (both built-in and third-party)
13+
2. Remove "controlled by automation software" infobar
14+
3. Support containerized environments (Docker, etc.)
15+
4. Maintain security while enabling usability
16+
17+
Reference: Nova Act SDK implementation
18+
"""
19+
20+
import os
21+
import sys
22+
import platform
23+
from typing import List, Dict, Any, Optional
24+
25+
26+
def is_containerized_environment() -> bool:
27+
"""
28+
Detect if running in a containerized environment (Docker, Kubernetes, etc.)
29+
30+
Returns:
31+
True if running in a container, False otherwise
32+
"""
33+
# Check for Docker
34+
if os.path.exists("/.dockerenv"):
35+
return True
36+
37+
# Check for environment variable
38+
if os.environ.get("DOCKER_CONTAINER") or os.environ.get("CONTAINER"):
39+
return True
40+
41+
# Check cgroup for container indicators
42+
try:
43+
with open("/proc/1/cgroup", "r") as f:
44+
content = f.read()
45+
if "docker" in content or "kubepods" in content or "containerd" in content:
46+
return True
47+
except (FileNotFoundError, PermissionError):
48+
pass
49+
50+
return False
51+
52+
53+
def get_base_browser_args(
54+
is_persistent_context: bool = True,
55+
window_size: Optional[tuple] = None,
56+
extra_args: Optional[List[str]] = None
57+
) -> List[str]:
58+
"""
59+
Get base browser arguments that should be used for all launches.
60+
61+
Args:
62+
is_persistent_context: Whether using persistent context (profile-based)
63+
window_size: Optional (width, height) tuple for window size
64+
extra_args: Additional custom arguments to include
65+
66+
Returns:
67+
List of browser arguments
68+
"""
69+
args = [
70+
# Prevent /dev/shm OOM issues in containers and memory-constrained environments
71+
"--disable-dev-shm-usage",
72+
]
73+
74+
# Add window size if specified
75+
if window_size:
76+
args.append(f"--window-size={window_size[0]},{window_size[1]}")
77+
78+
# Only add AutomationControlled flag for ephemeral (non-persistent) sessions
79+
# For persistent sessions, we want the browser to behave normally
80+
# This helps websites detect automation, but it's a tradeoff for profile stability
81+
if not is_persistent_context:
82+
args.append("--disable-blink-features=AutomationControlled")
83+
84+
# Cleaner debugging output (from Nova Act SDK)
85+
args.append("--silent-debugger-extension-api")
86+
87+
# Allow Chrome DevTools remote connections (useful for debugging)
88+
args.append("--remote-allow-origins=https://chrome-devtools-frontend.appspot.com")
89+
90+
# Add --no-sandbox only in containerized environments
91+
# Security: --no-sandbox reduces Chrome's security on non-containerized systems
92+
if is_containerized_environment():
93+
args.append("--no-sandbox")
94+
print("[INFO] Containerized environment detected, adding --no-sandbox", file=sys.stderr)
95+
96+
# Add user-provided extra arguments
97+
if extra_args:
98+
args.extend(extra_args)
99+
100+
# Add environment variable arguments (similar to Nova Act's NOVA_ACT_BROWSER_ARGS)
101+
env_args = os.environ.get("BROWSER_LAUNCH_ARGS", "").strip()
102+
if env_args:
103+
args.extend(env_args.split())
104+
print(f"[INFO] Added browser args from BROWSER_LAUNCH_ARGS: {env_args}", file=sys.stderr)
105+
106+
return args
107+
108+
109+
def get_ignore_default_args() -> List[str]:
110+
"""
111+
Get list of default Playwright arguments to ignore/remove.
112+
113+
These are removed to enable password manager functionality:
114+
- --enable-automation: When set, shows "controlled by automation" infobar
115+
and disables some password manager features
116+
- --disable-component-extensions-with-background-pages: When set, disables
117+
background extension pages which breaks password manager popup UI
118+
- --hide-scrollbars: Playwright hides scrollbars by default in headless,
119+
but we want them visible for better UX in screenshots
120+
121+
Returns:
122+
List of argument names to ignore
123+
"""
124+
return [
125+
# Remove automation flag to:
126+
# 1. Hide "Chrome is being controlled by automated test software" infobar
127+
# 2. Enable password manager save prompts
128+
# 3. Enable password autofill on subsequent visits
129+
"--enable-automation",
130+
131+
# Remove to enable password manager extension UI
132+
# This is CRITICAL for password managers that use popups/bubbles
133+
"--disable-component-extensions-with-background-pages",
134+
135+
# Show scrollbars for better UX (Nova Act SDK does this too)
136+
"--hide-scrollbars",
137+
]
138+
139+
140+
def get_launch_options(
141+
headless: bool = False,
142+
browser_channel: Optional[str] = None,
143+
is_persistent_context: bool = True,
144+
window_size: Optional[tuple] = None,
145+
ignore_https_errors: bool = True,
146+
extra_args: Optional[List[str]] = None
147+
) -> Dict[str, Any]:
148+
"""
149+
Get complete launch options for Playwright browser launch.
150+
151+
This is the main function to use for consistent browser launches.
152+
153+
Args:
154+
headless: Whether to run in headless mode
155+
browser_channel: Browser channel ('chrome', 'msedge', 'chromium', etc.)
156+
is_persistent_context: Whether using persistent context
157+
window_size: Optional (width, height) tuple
158+
ignore_https_errors: Whether to ignore HTTPS certificate errors
159+
extra_args: Additional custom arguments
160+
161+
Returns:
162+
Dictionary of launch options for Playwright
163+
164+
Example:
165+
```python
166+
from browser_launch_config import get_launch_options
167+
168+
options = get_launch_options(
169+
headless=False,
170+
browser_channel='msedge',
171+
is_persistent_context=True
172+
)
173+
174+
context = p.chromium.launch_persistent_context(
175+
user_data_dir,
176+
**options
177+
)
178+
```
179+
"""
180+
options = {
181+
"headless": headless,
182+
"args": get_base_browser_args(
183+
is_persistent_context=is_persistent_context,
184+
window_size=window_size,
185+
extra_args=extra_args
186+
),
187+
"ignore_default_args": get_ignore_default_args(),
188+
}
189+
190+
# Add browser channel if specified (and not 'chromium' which is the default)
191+
if browser_channel and browser_channel.lower() != "chromium":
192+
options["channel"] = browser_channel
193+
194+
# Add HTTPS error handling
195+
if ignore_https_errors:
196+
options["ignore_https_errors"] = True
197+
198+
return options
199+
200+
201+
def get_default_browser_channel() -> str:
202+
"""
203+
Get the default browser channel for the current platform.
204+
205+
Returns:
206+
'msedge' on Windows, 'chrome' on other platforms
207+
"""
208+
if platform.system() == "Windows":
209+
return "msedge"
210+
return "chrome"
211+
212+
213+
def log_launch_config(options: Dict[str, Any], context_type: str = "launch") -> None:
214+
"""
215+
Log the browser launch configuration for debugging.
216+
217+
Args:
218+
options: Launch options dictionary
219+
context_type: Description of launch context (e.g., "profile_setup", "script_execution")
220+
"""
221+
print(f"\n{'='*60}", file=sys.stderr)
222+
print(f"Browser Launch Configuration ({context_type})", file=sys.stderr)
223+
print(f"{'='*60}", file=sys.stderr)
224+
print(f" Headless: {options.get('headless', False)}", file=sys.stderr)
225+
print(f" Channel: {options.get('channel', 'chromium (default)')}", file=sys.stderr)
226+
print(f" Args: {options.get('args', [])}", file=sys.stderr)
227+
print(f" Ignored defaults: {options.get('ignore_default_args', [])}", file=sys.stderr)
228+
print(f" Container mode: {is_containerized_environment()}", file=sys.stderr)
229+
print(f"{'='*60}\n", file=sys.stderr)
230+
231+
232+
# Convenience constants for common configurations
233+
PROFILE_SETUP_OPTIONS = {
234+
"headless": False, # Must be visible for manual login
235+
"is_persistent_context": True,
236+
"ignore_https_errors": True,
237+
}
238+
239+
SCRIPT_EXECUTION_OPTIONS = {
240+
"headless": False, # Default to visible, can be overridden
241+
"is_persistent_context": True,
242+
"ignore_https_errors": True,
243+
}
244+
245+
EPHEMERAL_SESSION_OPTIONS = {
246+
"headless": False,
247+
"is_persistent_context": False, # Adds AutomationControlled flag
248+
"ignore_https_errors": True,
249+
}
250+
251+
252+
if __name__ == "__main__":
253+
# Test/demo the configuration
254+
print("Browser Launch Configuration Demo")
255+
print("-" * 40)
256+
257+
print("\n1. Profile Setup Options:")
258+
options = get_launch_options(**PROFILE_SETUP_OPTIONS, browser_channel="msedge")
259+
log_launch_config(options, "profile_setup")
260+
261+
print("\n2. Script Execution Options:")
262+
options = get_launch_options(**SCRIPT_EXECUTION_OPTIONS, browser_channel="chrome")
263+
log_launch_config(options, "script_execution")
264+
265+
print("\n3. Ephemeral Session Options:")
266+
options = get_launch_options(**EPHEMERAL_SESSION_OPTIONS)
267+
log_launch_config(options, "ephemeral")
268+
269+
print("\n4. Container Detection:")
270+
print(f" Is containerized: {is_containerized_environment()}")

lambda/tools/local-browser-agent/python/computer_agent_wrapper.py

Lines changed: 44 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -316,17 +316,35 @@ def execute_script(command: Dict[str, Any]) -> Dict[str, Any]:
316316
if not browser_channel:
317317
browser_channel = 'msedge' if platform.system() == 'Windows' else 'chrome'
318318

319-
# Resolve profile if specified
319+
# Resolve profile using tag-based resolution
320+
# Priority: profile_name (exact match) -> required_tags (AND logic) -> temp profile -> error
320321
user_data_dir = None
321322
session_config = command.get('session', {})
322323
if session_config and isinstance(session_config, dict):
323-
profile_name = session_config.get('profile_name')
324-
if profile_name:
325-
profile_manager = ProfileManager()
326-
clone_for_parallel = session_config.get('clone_for_parallel', False)
327-
profile_config = profile_manager.get_nova_act_config(profile_name, clone_for_parallel=clone_for_parallel)
328-
user_data_dir = profile_config["user_data_dir"]
329-
print(f"[INFO] Using profile '{profile_name}' with user_data_dir: {user_data_dir}", file=sys.stderr)
324+
profile_manager = ProfileManager()
325+
326+
# Use resolve_profile for tag-based resolution with fallback
327+
try:
328+
resolved_profile = profile_manager.resolve_profile(session_config, verbose=True)
329+
330+
if resolved_profile:
331+
# Successfully resolved a named profile
332+
clone_for_parallel = session_config.get('clone_for_parallel', False)
333+
profile_config = profile_manager.get_nova_act_config(
334+
resolved_profile["name"],
335+
clone_for_parallel=clone_for_parallel
336+
)
337+
user_data_dir = profile_config["user_data_dir"]
338+
print(f"[INFO] Resolved profile '{resolved_profile['name']}' (tags: {resolved_profile.get('tags', [])})", file=sys.stderr)
339+
print(f"[INFO] Using user_data_dir: {user_data_dir}", file=sys.stderr)
340+
else:
341+
# Using temporary profile (allow_temp_profile was True)
342+
print(f"[INFO] Using temporary profile (no persistent session)", file=sys.stderr)
343+
344+
except ValueError as e:
345+
# Profile resolution failed and temp not allowed
346+
print(f"[ERROR] Profile resolution failed: {e}", file=sys.stderr)
347+
raise
330348

331349
# Create executor
332350
executor = OpenAIPlaywrightExecutor(
@@ -491,32 +509,29 @@ def setup_login(command: Dict[str, Any]) -> Dict[str, Any]:
491509
# We don't need LLM for manual login, so no API key required
492510
import time
493511
from playwright.sync_api import sync_playwright
512+
from browser_launch_config import get_launch_options, log_launch_config
494513

495514
print(f"Opening browser for manual login (no API key required)...", file=sys.stderr)
496515

516+
# Get consistent launch options for profile setup
517+
# Key features:
518+
# - Removes --enable-automation (hides "controlled by automation" bar)
519+
# - Removes --disable-component-extensions-with-background-pages (enables password manager UI)
520+
# - Adds --no-sandbox in containerized environments
521+
launch_options = get_launch_options(
522+
headless=False, # Must be visible for manual login
523+
browser_channel=browser_channel,
524+
is_persistent_context=True,
525+
)
526+
log_launch_config(launch_options, "profile_setup")
527+
497528
with sync_playwright() as p:
498529
# Launch browser with user_data_dir for session persistence
499-
# Note: user_data_dir must be passed to launch_persistent_context, not to launch + new_context
500-
if browser_channel == 'msedge':
501-
context = p.chromium.launch_persistent_context(
502-
user_data_dir,
503-
channel='msedge',
504-
headless=False,
505-
ignore_https_errors=True,
506-
)
507-
elif browser_channel == 'chrome':
508-
context = p.chromium.launch_persistent_context(
509-
user_data_dir,
510-
channel='chrome',
511-
headless=False,
512-
ignore_https_errors=True,
513-
)
514-
else:
515-
context = p.chromium.launch_persistent_context(
516-
user_data_dir,
517-
headless=False,
518-
ignore_https_errors=True,
519-
)
530+
# Using shared launch configuration for password manager support
531+
context = p.chromium.launch_persistent_context(
532+
user_data_dir,
533+
**launch_options
534+
)
520535

521536
# Create page and navigate
522537
page = context.new_page()

0 commit comments

Comments
 (0)