Skip to content

Commit 2cab8bb

Browse files
guyernestclaude
andcommitted
feat(browser-agent): Fix profile login setup with NovaActExecutor
Major improvements to local browser agent configuration and profile management: ## Profile Login Setup - Add setup_login command type to nova_act_wrapper.py - Opens browser with headless=false for manual login - Uses time.sleep() instead of input() to avoid stdin issues - Automatically saves session after 5-minute timeout - Refactor profile_commands.rs to use NovaActExecutor (consistent with validate_profile) ## Path Resolution & Python Venv - Fix find_python_executable() to use venv symlink directly (not canonicalize) - Update all profile commands to use venv Python instead of uvx/python3 - Fix profile_manager.py path resolution for macOS app bundle - Add proper logging for Python and script paths ## Configuration Persistence - Remove strict validation from Config::from_file() - Allow incomplete configs to load (e.g., missing API key) - Add validate_for_polling() for strict validation when needed - Fix config reload in script execution for immediate effect ## Python Environment Setup - Add UV verification before use (test with --version) - Automatic fallback to PATH search if UV broken - Enhanced error messages with setup instructions ## Nova Act Authentication - Smart API key detection (arg, env var, boto session) - Avoid conflicts between API key and boto_session - Add comprehensive session startup logging ## Build & Deployment - Update Makefile with package target for DMG deployment - Add examples directory to Tauri bundle resources - Improve script_executor and test_commands path resolution ## UI Improvements - Better config loading logging in ConfigScreen.tsx - Enhanced Python environment setup progress display 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent bc6df8b commit 2cab8bb

File tree

11 files changed

+1308
-202
lines changed

11 files changed

+1308
-202
lines changed

lambda/tools/local-browser-agent/Makefile

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ help:
1414
@echo ""
1515
@echo "Development:"
1616
@echo " make dev Run in development mode with hot reload (opens Tauri UI)"
17-
@echo " make build Build release binary"
17+
@echo " make build Build release binary with DMG installer"
18+
@echo " make build-bin Build binary only (faster, no DMG bundle)"
19+
@echo " make package Create deployment package (includes DMG, Python, docs)"
1820
@echo " make run Run the agent (requires config.yaml)"
1921
@echo " make run-config Run with custom config (CONFIG=/path/to/config.yaml)"
2022
@echo ""
@@ -89,10 +91,82 @@ install-rust:
8991
# Build
9092
build:
9193
@echo "Building Local Browser Agent..."
94+
@# Remove .venv from python directory to avoid bundling it (320MB!)
95+
@rm -rf python/.venv python/__pycache__ python/*.pyc
96+
@cd src-tauri && cargo tauri build
97+
@echo ""
98+
@echo "Build complete!"
99+
@echo " Binary: ./src-tauri/target/release/local-browser-agent"
100+
@echo " App: ./src-tauri/target/release/bundle/macos/Local Browser Agent.app"
101+
@echo " DMG: ./src-tauri/target/release/bundle/dmg/*.dmg"
102+
103+
# Build binary only (faster, no bundle)
104+
build-bin:
105+
@echo "Building binary only (no bundle)..."
92106
@cd ui && npm run build
93107
@cd src-tauri && cargo build --release
94108
@echo "Build complete: ./src-tauri/target/release/local-browser-agent"
95109

110+
# Create deployment package
111+
package:
112+
@echo "=================================="
113+
@echo "Creating deployment package..."
114+
@echo "=================================="
115+
@echo ""
116+
@# Check if build exists
117+
@if [ ! -f "src-tauri/target/release/bundle/macos/Local Browser Agent.app/Contents/MacOS/local-browser-agent" ]; then \
118+
echo "❌ Build not found. Running 'make build' first..."; \
119+
make build; \
120+
fi
121+
@# Create deployment directory
122+
@rm -rf deployment-package
123+
@mkdir -p deployment-package
124+
@echo "📦 Packaging files..."
125+
@# Copy DMG installer
126+
@echo " - DMG installer"
127+
@cp src-tauri/target/release/bundle/dmg/*.dmg deployment-package/ 2>/dev/null || true
128+
@# Copy app bundle
129+
@echo " - App bundle"
130+
@cp -r "src-tauri/target/release/bundle/macos/Local Browser Agent.app" deployment-package/ 2>/dev/null || true
131+
@# Copy Python directory
132+
@echo " - Python scripts"
133+
@mkdir -p deployment-package/python
134+
@cp python/*.py deployment-package/python/
135+
@cp python/requirements.txt deployment-package/python/
136+
@# Copy examples directory
137+
@echo " - Examples"
138+
@cp -r examples deployment-package/
139+
@# Copy configuration template
140+
@echo " - Configuration template"
141+
@cp config.example.yaml deployment-package/config.yaml
142+
@# Create setup script
143+
@echo " - Setup script"
144+
@cp SETUP.sh.template deployment-package/SETUP.sh
145+
@chmod +x deployment-package/SETUP.sh
146+
@# Copy README template
147+
@echo " - README"
148+
@cp README.template.md deployment-package/README.md
149+
@# Create compressed archive
150+
@echo " - Creating archive"
151+
@tar -czf browser-agent-deployment.tar.gz deployment-package/
152+
@# Show results
153+
@echo ""
154+
@echo "=================================="
155+
@echo "✅ Deployment Package Created!"
156+
@echo "=================================="
157+
@echo ""
158+
@echo "Archive: browser-agent-deployment.tar.gz"
159+
@ls -lh browser-agent-deployment.tar.gz
160+
@echo ""
161+
@echo "Package contents:"
162+
@ls -lh deployment-package/
163+
@echo ""
164+
@echo "To deploy to UK Mac:"
165+
@echo " 1. Transfer: scp browser-agent-deployment.tar.gz user@uk-mac:"
166+
@echo " 2. Extract: tar -xzf browser-agent-deployment.tar.gz"
167+
@echo " 3. Setup: cd deployment-package && ./SETUP.sh"
168+
@echo ""
169+
96170
# Development
97171
dev:
98172
@echo "Starting development server..."

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

Lines changed: 146 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def execute_browser_command(command: Dict[str, Any]) -> Dict[str, Any]:
2929
3030
Args:
3131
command: Dictionary containing:
32-
- command_type: 'start_session', 'act', 'script', 'end_session', or 'validate_profile'
32+
- command_type: 'start_session', 'act', 'script', 'end_session', 'validate_profile', or 'setup_login'
3333
- prompt: Natural language instruction (for 'act')
3434
- steps: List of script steps (for 'script')
3535
- starting_page: Initial URL (for 'start_session' or 'script')
@@ -41,6 +41,7 @@ def execute_browser_command(command: Dict[str, Any]) -> Dict[str, Any]:
4141
- schema: JSON schema for output
4242
- headless: Run headless mode
4343
- record_video: Record video
44+
- profile_name: Profile name (for 'setup_login')
4445
4546
Returns:
4647
Dictionary with execution result
@@ -58,6 +59,8 @@ def execute_browser_command(command: Dict[str, Any]) -> Dict[str, Any]:
5859
return end_session(command)
5960
elif command_type == 'validate_profile':
6061
return validate_profile(command)
62+
elif command_type == 'setup_login':
63+
return setup_login(command)
6164
else:
6265
return {
6366
"success": False,
@@ -160,14 +163,16 @@ def execute_act(command: Dict[str, Any]) -> Dict[str, Any]:
160163
print("[DEMO MODE] Forcing profile to: Bt_broadband", file=sys.stderr)
161164
from profile_manager import ProfileManager
162165
profile_manager = ProfileManager()
166+
profile_name = "Bt_broadband"
163167
try:
164-
profile_config = profile_manager.get_nova_act_config("Bt_broadband", clone_for_parallel=False)
168+
profile_config = profile_manager.get_nova_act_config(profile_name, clone_for_parallel=False)
165169
user_data_dir = profile_config["user_data_dir"]
166170
clone_user_data_dir = profile_config["clone_user_data_dir"]
167-
print(f"[DEMO MODE] Using profile path: {user_data_dir}", file=sys.stderr)
171+
print(f"[DEMO MODE] Loaded profile '{profile_name}' from path: {user_data_dir}", file=sys.stderr)
168172
except Exception as e:
169-
print(f"[DEMO MODE] Warning: Could not load Bt_broadband profile: {e}", file=sys.stderr)
173+
print(f"[DEMO MODE] Warning: Could not load {profile_name} profile: {e}", file=sys.stderr)
170174
print(f"[DEMO MODE] Continuing with original user_data_dir: {user_data_dir}", file=sys.stderr)
175+
profile_name = "default" if not user_data_dir else "custom"
171176

172177
# Create boto3 session with local credentials
173178
boto_session = boto3.Session(profile_name=aws_profile)
@@ -213,6 +218,18 @@ def execute_act(command: Dict[str, Any]) -> Dict[str, Any]:
213218
else:
214219
nova_act_kwargs["boto_session"] = boto_session
215220

221+
# Log comprehensive session startup information
222+
print(f"[INFO] Starting browser session with:", file=sys.stderr)
223+
print(f" - Profile: {profile_name}", file=sys.stderr)
224+
print(f" - Profile Path: {user_data_dir}", file=sys.stderr)
225+
print(f" - Headless Mode: {headless}", file=sys.stderr)
226+
print(f" - Clone Profile: {clone_user_data_dir if clone_user_data_dir is not None else 'not set (NovaAct default)'}", file=sys.stderr)
227+
print(f" - Starting Page: {starting_page or 'none'}", file=sys.stderr)
228+
print(f" - Record Video: {record_video}", file=sys.stderr)
229+
print(f" - Max Steps: {max_steps}", file=sys.stderr)
230+
print(f" - Timeout: {timeout}s", file=sys.stderr)
231+
print(f" - Session ID: {session_id or 'auto-generated'}", file=sys.stderr)
232+
216233
with NovaAct(**nova_act_kwargs) as nova:
217234
# Execute act command
218235
result = nova.act(
@@ -503,6 +520,131 @@ def validate_profile(command: Dict[str, Any]) -> Dict[str, Any]:
503520
return out
504521

505522

523+
def setup_login(command: Dict[str, Any]) -> Dict[str, Any]:
524+
"""Setup interactive login for a profile.
525+
526+
Opens a browser with the profile's user_data_dir, navigates to the starting_url,
527+
and waits for a timeout period to allow the user to manually log in.
528+
When the browser closes or timeout expires, the session is automatically saved.
529+
530+
Args:
531+
command: Dictionary containing:
532+
- profile_name: Name of the profile
533+
- starting_url: URL to navigate to for login
534+
- timeout: Timeout in seconds (default: 300 = 5 minutes)
535+
536+
Returns:
537+
Dictionary with success status
538+
"""
539+
profile_name = command.get("profile_name")
540+
starting_url = command.get("starting_url")
541+
542+
if not profile_name:
543+
return {"success": False, "error": "profile_name is required"}
544+
if not starting_url:
545+
return {"success": False, "error": "starting_url is required"}
546+
547+
try:
548+
from profile_manager import ProfileManager
549+
550+
# Initialize profile manager
551+
profile_manager = ProfileManager()
552+
553+
# Check if profile exists, create if not
554+
profile = profile_manager.get_profile(profile_name)
555+
if not profile:
556+
print(f"Creating new profile: {profile_name}", file=sys.stderr)
557+
profile = profile_manager.create_profile(
558+
profile_name=profile_name,
559+
description=f"Profile with authenticated session for {starting_url}",
560+
tags=["authenticated"],
561+
auto_login_sites=[starting_url]
562+
)
563+
else:
564+
print(f"Using existing profile: {profile_name}", file=sys.stderr)
565+
566+
# Mark profile as requiring human login
567+
profile_manager.mark_profile_for_login(
568+
profile_name=profile_name,
569+
requires_human=True,
570+
notes=f"Manual login required for {starting_url}"
571+
)
572+
573+
# Get Nova Act config
574+
config = profile_manager.get_nova_act_config(profile_name, clone_for_parallel=False)
575+
user_data_dir = config["user_data_dir"]
576+
577+
# Determine timeout (default 5 minutes)
578+
timeout = command.get("timeout", 300)
579+
580+
# Get authentication credentials
581+
nova_act_api_key = os.environ.get('NOVA_ACT_API_KEY')
582+
boto_session = None
583+
if not nova_act_api_key:
584+
try:
585+
aws_profile = command.get('aws_profile', 'browser-agent')
586+
boto_session = boto3.Session(profile_name=aws_profile)
587+
except Exception:
588+
pass
589+
590+
# Build NovaAct kwargs
591+
nova_act_kwargs = {
592+
"starting_page": starting_url,
593+
"user_data_dir": user_data_dir,
594+
"clone_user_data_dir": False, # Don't clone to preserve session
595+
"headless": False, # Must be visible for human login
596+
"ignore_https_errors": True,
597+
}
598+
599+
if nova_act_api_key:
600+
nova_act_kwargs["nova_act_api_key"] = nova_act_api_key
601+
elif boto_session:
602+
nova_act_kwargs["boto_session"] = boto_session
603+
604+
print(f"", file=sys.stderr)
605+
print(f"╔═══════════════════════════════════════════════════════════╗", file=sys.stderr)
606+
print(f"║ PROFILE LOGIN SETUP ║", file=sys.stderr)
607+
print(f"╠═══════════════════════════════════════════════════════════╣", file=sys.stderr)
608+
print(f"║ Profile: {profile_name:<48}║", file=sys.stderr)
609+
print(f"║ URL: {starting_url[:48]:<48}║", file=sys.stderr)
610+
print(f"║ Timeout: {timeout} seconds{'':<38}║", file=sys.stderr)
611+
print(f"╠═══════════════════════════════════════════════════════════╣", file=sys.stderr)
612+
print(f"║ A browser window will open. Please log in manually. ║", file=sys.stderr)
613+
print(f"║ The browser will stay open for {timeout//60} minutes.{'':<23}║", file=sys.stderr)
614+
print(f"║ Your login session will be saved automatically. ║", file=sys.stderr)
615+
print(f"║ You can close the browser when done. ║", file=sys.stderr)
616+
print(f"╚═══════════════════════════════════════════════════════════╝", file=sys.stderr)
617+
print(f"", file=sys.stderr)
618+
619+
# Open browser for manual login
620+
import time
621+
with NovaAct(**nova_act_kwargs) as nova:
622+
# Just wait for the timeout - user can log in during this time
623+
# The browser will stay open and visible
624+
print(f"Browser opened. Waiting {timeout} seconds for you to complete login...", file=sys.stderr)
625+
time.sleep(timeout)
626+
print(f"Timeout reached. Closing browser and saving session...", file=sys.stderr)
627+
628+
print(f"", file=sys.stderr)
629+
print(f"✓ Profile '{profile_name}' login setup completed!", file=sys.stderr)
630+
print(f" User data directory: {user_data_dir}", file=sys.stderr)
631+
print(f" Future scripts can reuse this authenticated session", file=sys.stderr)
632+
633+
return {
634+
"success": True,
635+
"message": f"Login setup completed for profile '{profile_name}'",
636+
"profile_name": profile_name,
637+
"user_data_dir": user_data_dir,
638+
}
639+
640+
except Exception as e:
641+
return {
642+
"success": False,
643+
"error": f"Failed to setup login: {str(e)}",
644+
"traceback": traceback.format_exc()
645+
}
646+
647+
506648
def main():
507649
"""
508650
Main entry point

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

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -229,15 +229,38 @@ def execute_script(self, script: Dict[str, Any]) -> Dict[str, Any]:
229229

230230
# Add Nova Act API key if provided (otherwise uses NOVA_ACT_API_KEY env var)
231231
# Note: Cannot use both API key and boto_session
232-
if self.nova_act_api_key:
233-
nova_act_kwargs["nova_act_api_key"] = self.nova_act_api_key
232+
# Check for API key from: 1) argument, 2) env var
233+
api_key_from_arg = self.nova_act_api_key and self.nova_act_api_key.strip()
234+
api_key_from_env = os.environ.get("NOVA_ACT_API_KEY")
235+
236+
if api_key_from_arg:
237+
# Explicit API key provided via argument
238+
nova_act_kwargs["nova_act_api_key"] = self.nova_act_api_key.strip()
239+
elif api_key_from_env:
240+
# API key in environment - NovaAct will pick it up automatically
241+
# Do NOT pass boto_session as it conflicts with API key
242+
pass
234243
else:
235-
# Use boto session for IAM-based auth (requires allowlist)
244+
# No API key - use boto session for IAM-based auth (requires allowlist)
236245
nova_act_kwargs["boto_session"] = self.boto_session
237246

238247
# Track active user_data_dir for validation steps
239248
self._active_user_data_dir = profile_user_data_dir
240249

250+
# Log comprehensive session startup information
251+
print(f"[INFO] Starting browser session with:", file=sys.stderr)
252+
print(f" - Script: {name}", file=sys.stderr)
253+
print(f" - Profile: {profile_name}", file=sys.stderr)
254+
print(f" - Profile Path: {profile_user_data_dir}", file=sys.stderr)
255+
print(f" - Headless Mode: {nova_act_kwargs['headless']}", file=sys.stderr)
256+
print(f" - Clone Profile: {clone_user_data_dir}", file=sys.stderr)
257+
print(f" - Starting Page: {starting_page}", file=sys.stderr)
258+
print(f" - Record Video: {self.record_video}", file=sys.stderr)
259+
print(f" - Max Steps per Action: {self.max_steps}", file=sys.stderr)
260+
print(f" - Timeout per Action: {self.timeout}s", file=sys.stderr)
261+
print(f" - Navigation Timeout: {nova_act_kwargs.get('go_to_url_timeout', 'default')}s", file=sys.stderr)
262+
print(f" - Total Steps: {len(steps)}", file=sys.stderr)
263+
241264
# Create NovaAct instance
242265
nova = NovaAct(**nova_act_kwargs)
243266
nova.start()

lambda/tools/local-browser-agent/src-tauri/src/config.rs

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,21 @@ fn default_heartbeat_interval() -> u64 {
4545
}
4646

4747
impl Config {
48+
/// Create a default minimal config (for when no config file exists)
49+
pub fn default_minimal() -> Self {
50+
Config {
51+
activity_arn: String::new(),
52+
aws_profile: "default".to_string(),
53+
s3_bucket: String::new(),
54+
user_data_dir: None,
55+
ui_port: default_ui_port(),
56+
nova_act_api_key: None,
57+
headless: false,
58+
heartbeat_interval: default_heartbeat_interval(),
59+
aws_region: None,
60+
}
61+
}
62+
4863
/// Load configuration from YAML file
4964
pub fn from_file(path: &PathBuf) -> Result<Self> {
5065
let contents = std::fs::read_to_string(path)
@@ -53,23 +68,38 @@ impl Config {
5368
let config: Config = serde_yaml::from_str(&contents)
5469
.context("Failed to parse config file")?;
5570

56-
config.validate()?;
71+
// Don't validate here - allow incomplete configs for testing/configuration
72+
// Validation will happen when trying to use specific features
5773

5874
Ok(config)
5975
}
6076

61-
/// Validate configuration
62-
fn validate(&self) -> Result<()> {
77+
/// Validate configuration for activity polling
78+
/// This is stricter than basic validation - requires all fields for polling to work
79+
pub fn validate_for_polling(&self) -> Result<()> {
6380
if self.activity_arn.is_empty() {
64-
anyhow::bail!("activity_arn cannot be empty");
81+
anyhow::bail!("activity_arn cannot be empty for activity polling");
6582
}
6683

6784
if self.aws_profile.is_empty() {
6885
anyhow::bail!("aws_profile cannot be empty");
6986
}
7087

7188
if self.s3_bucket.is_empty() {
72-
anyhow::bail!("s3_bucket cannot be empty");
89+
anyhow::bail!("s3_bucket cannot be empty for activity polling");
90+
}
91+
92+
if self.heartbeat_interval == 0 {
93+
anyhow::bail!("heartbeat_interval must be greater than 0");
94+
}
95+
96+
Ok(())
97+
}
98+
99+
/// Basic validation - just ensures critical fields are valid
100+
fn validate(&self) -> Result<()> {
101+
if self.aws_profile.is_empty() {
102+
anyhow::bail!("aws_profile cannot be empty");
73103
}
74104

75105
if self.heartbeat_interval == 0 {

0 commit comments

Comments
 (0)