Skip to content

Commit 8e01803

Browse files
committed
SandboxEnvironment reusing context configs from launch.py
1 parent fe4ab37 commit 8e01803

File tree

4 files changed

+65
-93
lines changed

4 files changed

+65
-93
lines changed

src/browser/browser.py

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -204,17 +204,6 @@ async def launch_browser(self, task_id: int) -> BrowserContext:
204204
"--password-store=basic",
205205
]
206206

207-
# TODO: environment.py should be cleaned and reuse more of launch.py
208-
# TODO: does the agent launch works?
209-
# TODO: does the agent when evaluated works on the environment?
210-
# TODO: improve launching and running the environment
211-
212-
# ====== once this works well ======
213-
214-
# TODO: collect env with further n steps depth, using replay to bypass auths sections
215-
# TODO: eval runs in parallel containers, or ran on kernel, hosting tunneled versions locally while it runs?
216-
# TODO: websockets? like e.g. ChatGPT doesn't allow for collecting anything
217-
218207
browser = await self.playwright.chromium.launch(
219208
channel=preferred_channel,
220209
headless=False,

src/environments/environment.py

Lines changed: 31 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import http.client
33
import json
44
import logging
5-
import os
65
import socket
76
from pathlib import Path
87
from typing import List, Optional
@@ -11,7 +10,7 @@
1110
from playwright.async_api import BrowserContext, BrowserType, async_playwright
1211

1312
from environments.launch import ReplayBundle
14-
from config.browser_config import BROWSER_ARGS, CONTEXT_CONFIG
13+
from config.browser_config import BROWSER_ARGS
1514

1615

1716
logger = logging.getLogger(__name__)
@@ -71,41 +70,28 @@ def __init__(
7170
bundle_path: Path,
7271
*,
7372
allow_network_fallback: bool = False,
74-
headless: Optional[bool] = None,
75-
browser_args: Optional[List[str]] = None,
73+
headless: bool = False,
7674
safe_mode: bool = False,
75+
browser_args: Optional[List[str]] = None,
76+
include_storage_state: bool = False,
7777
) -> None:
7878
self.bundle = ReplayBundle(bundle_path)
7979
self.allow_network_fallback = allow_network_fallback
8080
self.safe_mode = safe_mode
81-
82-
# TODO: what is this needed for?
83-
env_headless = os.environ.get("SANDBOX_HEADLESS")
84-
env_safe_mode = os.environ.get("SANDBOX_SAFE_MODE")
85-
# TODO: ignore this env vars bullshit
86-
# TODO: some variables naming don't elicit anything
87-
if env_safe_mode is not None:
88-
self.safe_mode = env_safe_mode.lower() in {"1", "true", "yes", "on"}
89-
90-
# Determine headless setting (allow override even in safe mode)
91-
if env_headless is not None:
92-
self.headless = env_headless.lower() in {"1", "true", "yes", "on"}
93-
else:
94-
self.headless = headless if headless is not None else False
81+
self.headless = headless
82+
self.include_storage_state = include_storage_state
9583

9684
# Set browser args based on mode
9785
if self.safe_mode:
98-
base_args = SAFE_BROWSER_ARGS
86+
self.browser_args = SAFE_BROWSER_ARGS
87+
elif browser_args is not None:
88+
self.browser_args = browser_args
9989
else:
100-
base_args = browser_args if browser_args is not None else BROWSER_ARGS
101-
102-
if browser_args is not None and self.safe_mode:
103-
base_args = browser_args
104-
105-
self.browser_args = list(base_args) if base_args else []
90+
self.browser_args = BROWSER_ARGS
10691

10792
self._playwright = None
10893
self._browser: Optional[PlaywrightBrowser] = None
94+
# TODO: a list of contexts seem like an overkill
10995
self._contexts: list[BrowserContext] = []
11096
self._ws_endpoint: Optional[str] = None
11197
self._debug_port: Optional[int] = None
@@ -135,11 +121,7 @@ async def start(self) -> str:
135121
self.headless,
136122
)
137123

138-
launch_kwargs = {
139-
"headless": self.headless,
140-
"args": launch_args,
141-
}
142-
124+
launch_kwargs = {"headless": self.headless, "args": launch_args}
143125
self._browser = await browser_type.launch(**launch_kwargs)
144126
self._browser.on(
145127
"context",
@@ -148,7 +130,11 @@ async def start(self) -> str:
148130

149131
# Ensure at least one context exists for routing
150132
if not self._browser.contexts:
151-
context = await self._browser.new_context(**CONTEXT_CONFIG)
133+
# TODO: or should use **CONTEXT_CONFIG?
134+
context_config = self.bundle.get_context_config(
135+
include_storage_state=self.include_storage_state
136+
)
137+
context = await self._browser.new_context(**context_config)
152138
await self._configure_context(context)
153139
else:
154140
for context in list(self._browser.contexts):
@@ -205,35 +191,26 @@ def _fetch() -> Optional[str]:
205191
raise RuntimeError("Timed out waiting for Chrome debugger endpoint")
206192

207193
async def _configure_context(self, context: BrowserContext) -> None:
194+
"""Configure a context with HAR replay using the bundle's configuration."""
208195
if context in self._contexts:
209196
return
210197
self._contexts.append(context)
211198

212-
# Configure HAR-based replay
213-
har_path = self.bundle.bundle_path / "recording.har"
214-
if har_path.exists():
215-
logger.info("[SANDBOX] Using HAR replay from %s", har_path)
216-
await context.route_from_har(
217-
str(har_path),
218-
not_found="fallback" if self.allow_network_fallback else "abort",
219-
update=False,
220-
)
221-
else:
222-
raise FileNotFoundError(
223-
f"[SANDBOX] HAR file not found at {har_path}. Cannot replay without HAR file."
224-
)
199+
# Delegate to the bundle's configure_context method for DRY
200+
await self.bundle.configure_context(
201+
context,
202+
allow_network_fallback=self.allow_network_fallback,
203+
)
225204

226205
async def close(self) -> None:
227-
if self._browser:
228-
try:
229-
await self._browser.close()
230-
except Exception:
231-
pass
232-
if self._playwright:
233-
try:
234-
await self._playwright.stop()
235-
except Exception:
236-
pass
206+
try:
207+
await self._browser.close()
208+
except Exception:
209+
pass
210+
try:
211+
await self._playwright.stop()
212+
except Exception:
213+
pass
237214

238215
self._browser = None
239216
self._playwright = None

src/environments/launch.py

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@
88

99
from db.step import StepManager
1010
import typer
11-
from playwright.async_api import Browser, BrowserContext, Request, async_playwright
11+
from playwright.async_api import (
12+
Browser,
13+
BrowserContext,
14+
Request,
15+
Route,
16+
async_playwright,
17+
)
1218

1319
from environments.replay import TaskStepExecutor
1420
from config.storage import DATA_DIR
@@ -206,7 +212,21 @@ async def build_context(
206212
include_storage_state: bool = False,
207213
) -> BrowserContext:
208214
"""Build a browser context with HAR-based replay."""
215+
context_config = self.get_context_config(
216+
include_storage_state=include_storage_state
217+
)
218+
context = await browser.new_context(**context_config)
219+
await self.configure_context(
220+
context, allow_network_fallback=allow_network_fallback
221+
)
222+
return context
223+
224+
def get_context_config(
225+
self, *, include_storage_state: bool = False
226+
) -> Dict[str, Any]:
227+
"""Prepare context configuration, optionally including storage state."""
209228
context_config = dict(self.environment.get("context_config") or {})
229+
210230
if include_storage_state:
211231
storage_state_path = self._storage_state_path()
212232
if storage_state_path:
@@ -215,7 +235,15 @@ async def build_context(
215235
logger.warning("Storage state file not found, using empty state")
216236
context_config["storage_state"] = "{}"
217237

218-
context = await browser.new_context(**context_config)
238+
return context_config
239+
240+
async def configure_context(
241+
self,
242+
context: BrowserContext,
243+
*,
244+
allow_network_fallback: bool = False,
245+
) -> None:
246+
"""Configure an existing browser context with HAR replay and routing."""
219247
self._setup_har_logging(context)
220248

221249
har_path = self.bundle_path / "recording.har"
@@ -235,9 +263,8 @@ async def build_context(
235263
await context.route(
236264
"**/*", lambda route, request: self.handle_routes_manually(route, request)
237265
)
238-
return context
239266

240-
async def handle_routes_manually(self, route, request):
267+
async def handle_routes_manually(self, route: Route, request: Request) -> None:
241268
# TODO: do we need to obsfucate in a more clever way?
242269
# - ?? Normalize JSON (remove volatile fields; sort keys) and hash; tolerate multipart boundary changes; ignore known nonce/timestamp params.
243270
# TODO: what if the request is sent twice, we'll be selecting the first one all the time.
@@ -246,6 +273,7 @@ async def handle_routes_manually(self, route, request):
246273
# TODO: this requires LM postprocessing selection of URL's to match or some dumb way for all POST? or smth
247274
# TODO: why when collecting, increasing/decreasing cart stuff fails
248275
# TODO: some assets in GET are also dynamic?, bunch of js/stylesheets are not found in HAR
276+
# TODO: websockets? like e.g. ChatGPT doesn't allow for collecting anything
249277

250278
urls_to_ignore_post_data = {
251279
"https://www.amazon.com/ax/claim",

src/eval/run/browseruse.py

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,6 @@ async def run_task_with_agent(
239239
sandbox_bundle: Optional[Path] = None,
240240
sandbox_allow_network: bool = False,
241241
sandbox_headless: bool = True,
242-
sandbox_safe_mode: bool = False,
243242
) -> Dict[str, Any]:
244243
"""Run a single task with the Browser-Use agent and capture all data."""
245244

@@ -285,14 +284,9 @@ def capture_accessibility_tree(browser_state, agent_output, step_number):
285284

286285
try:
287286
sandbox_start_error: Optional[Exception] = None
288-
sandbox_modes = []
289-
if sandbox_bundle:
290-
if sandbox_safe_mode:
291-
sandbox_modes = [True]
292-
else:
293-
sandbox_modes = [False, True]
287+
for safe_mode in [False, True]:
288+
# try first antidetection and performance, if fails, try safe mode
294289

295-
for safe_mode in sandbox_modes:
296290
logger.info(
297291
"Starting sandbox for task %s at %s (safe_mode=%s)",
298292
task["task_id"],
@@ -461,7 +455,6 @@ async def process_single_task(
461455
sandbox_root: Optional[Path],
462456
sandbox_allow_network: bool,
463457
sandbox_headless: bool,
464-
sandbox_safe_mode: bool,
465458
semaphore: Optional[asyncio.Semaphore] = None,
466459
):
467460
"""Process a single task and write results to individual JSON file"""
@@ -495,7 +488,6 @@ async def process_single_task(
495488
sandbox_bundle=sandbox_bundle,
496489
sandbox_allow_network=sandbox_allow_network,
497490
sandbox_headless=sandbox_headless,
498-
sandbox_safe_mode=sandbox_safe_mode,
499491
)
500492

501493
# Write result to individual JSON file
@@ -534,7 +526,6 @@ async def process_all_tasks(
534526
sandbox_root: Optional[Path],
535527
sandbox_allow_network: bool,
536528
sandbox_headless: bool,
537-
sandbox_safe_mode: bool,
538529
):
539530
"""Process all tasks and save to individual JSON files, skipping already completed ones"""
540531
# Cleanup all active Kernel browser sessions before starting
@@ -593,7 +584,6 @@ async def process_all_tasks(
593584
sandbox_root=sandbox_root,
594585
sandbox_allow_network=sandbox_allow_network,
595586
sandbox_headless=sandbox_headless,
596-
sandbox_safe_mode=sandbox_safe_mode,
597587
semaphore=semaphore,
598588
)
599589
)
@@ -620,19 +610,12 @@ async def main(args: argparse.Namespace) -> None:
620610
)
621611

622612
sandbox_headless = not args.sandbox_headed
623-
sandbox_safe_mode = args.sandbox_safe_mode
624-
if sandbox_safe_mode and args.sandbox_headed:
625-
logger.warning(
626-
"Sandbox safe mode forces headless Chromium; ignoring --sandbox-headed"
627-
)
628-
sandbox_headless = True
629613

630614
results_dir = await process_all_tasks(
631615
args.model,
632616
sandbox_root=sandbox_root,
633617
sandbox_allow_network=args.sandbox_allow_network,
634618
sandbox_headless=sandbox_headless,
635-
sandbox_safe_mode=sandbox_safe_mode,
636619
)
637620
print(f"\nAll results saved to: {results_dir}")
638621

@@ -658,11 +641,6 @@ def parse_args() -> argparse.Namespace:
658641
action="store_true",
659642
help="Launch sandbox Chromium with a visible window",
660643
)
661-
parser.add_argument(
662-
"--sandbox-safe-mode",
663-
action="store_true",
664-
help="Use a reduced argument set and headless Chromium for stability",
665-
)
666644
return parser.parse_args()
667645

668646

0 commit comments

Comments
 (0)