Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion browser_use/browser/watchdogs/local_browser_watchdog.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,23 @@
pass


CDP_READY_TIMEOUT_ENV = 'TIMEOUT_BrowserCDPReady'
DEFAULT_CDP_READY_TIMEOUT = 180.0


def cdp_ready_timeout() -> float:
"""Return the timeout for local Chromium to expose its CDP endpoint."""
value = os.getenv(CDP_READY_TIMEOUT_ENV)
if value:
try:
parsed = float(value)
if parsed >= 0:
return parsed
except ValueError:
pass
return DEFAULT_CDP_READY_TIMEOUT


class LocalBrowserWatchdog(BaseWatchdog):
"""Manages local browser subprocess lifecycle."""

Expand Down Expand Up @@ -155,7 +172,7 @@ async def _launch_browser(self, max_retries: int = 3) -> tuple[psutil.Process, s
process = psutil.Process(subprocess.pid)

# Wait for CDP to be ready and get the URL
cdp_url = await self._wait_for_cdp_url(debug_port)
cdp_url = await self._wait_for_cdp_url(debug_port, timeout=cdp_ready_timeout())

# Success! Clean up only the temp dirs we created but didn't use
currently_used_dir = str(profile.user_data_dir)
Expand Down
17 changes: 14 additions & 3 deletions docker/Dockerfile.api
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@
# Required env vars: MURPHY_API_KEY, OPENAI_API_KEY
# Optional env vars: MURPHY_MAX_CONCURRENT_JOBS (default: 2), MURPHY_API_PORT (default: 8000)

FROM python:3.11-slim
FROM python:3.12-slim

ENV DEBIAN_FRONTEND=noninteractive \
PYTHONUNBUFFERED=1 \
UV_CACHE_DIR=/root/.cache/uv \
UV_LINK_MODE=copy \
UV_PYTHON_PREFERENCE=only-system \
CHROME_PATH=/usr/bin/chromium

# Install Chromium + minimal deps
Expand All @@ -33,12 +34,22 @@ WORKDIR /app

# Install dependencies first (layer caching)
COPY pyproject.toml uv.lock* /app/
RUN uv venv --python 3.11 && uv sync --no-dev --no-install-project
RUN uv venv --python 3.12 && uv sync --no-dev --no-install-project

# Copy source
COPY . /app
RUN uv sync --no-dev

# Create non-root user 1000:1000 with a home directory so browser-use's
# config/profile dirs (~/.config/browseruse, ~/.cache, etc.) have a writable
# location when ECS runs the container as user 1000.
RUN groupadd --gid 1000 murphy && \
useradd --uid 1000 --gid 1000 --create-home --home-dir /home/murphy murphy && \
chown -R murphy:murphy /app /home/murphy

ENV HOME=/home/murphy
USER murphy:murphy

EXPOSE 8000

CMD ["uv", "run", "uvicorn", "murphy.api:app", "--host", "0.0.0.0", "--port", "8000", "--timeout-keep-alive", "1800"]
CMD ["/app/.venv/bin/uvicorn", "murphy.api.rest:app", "--host", "0.0.0.0", "--port", "8000", "--timeout-keep-alive", "1800"]
2 changes: 2 additions & 0 deletions docs/BROWSER_USE_MODIFICATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@

13. **Broader empty-response detection** (`browser_use/llm/openai/chat.py`) — Changed empty-content check from `is None` to falsy (`not content`) to also catch empty strings; updated error message and status code to 502 (provider-side issue).

14. **Configurable local CDP readiness timeout** (`browser_use/browser/watchdogs/local_browser_watchdog.py`) — Added `TIMEOUT_BrowserCDPReady` with a 180-second default so slower containerized Chromium startups can wait longer for `/json/version` without modifying the event-level browser start timeouts.

## Syncing with Upstream

```bash
Expand Down
2 changes: 1 addition & 1 deletion murphy/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Murphy — AI-driven website evaluation powered by browser-use."""

__version__ = '1.1.0'
__version__ = '1.1.3'

from murphy.core.analysis import analyze_website as analyze_website
from murphy.core.execution import execute_tests as execute_tests
Expand Down
18 changes: 13 additions & 5 deletions murphy/api/jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,14 +107,22 @@ async def _acquire_semaphore() -> bool:

async def _execute_with_semaphore(job: Job, core_fn: Any, req: Any, timeout: int | float) -> None:
"""Run core_fn under semaphore, update job status on completion/failure."""
task: asyncio.Task[Any] | None = None
try:
effective = _effective_timeout(timeout)
job.result = await asyncio.wait_for(core_fn(req), timeout=effective)
task = asyncio.create_task(core_fn(req))
job.result = await asyncio.wait_for(task, timeout=effective)
job.status = 'completed'
except TimeoutError:
logger.error('Job %s timed out after %ds', job.id, _effective_timeout(timeout))
job.status = 'failed'
job.error = f'Job timed out after {_effective_timeout(timeout)}s'
except TimeoutError as exc:
if task is not None and task.done() and not task.cancelled():
tb = traceback.format_exc()
logger.error('Job %s failed: %s\n%s', job.id, exc, tb)
job.status = 'failed'
job.error = f'{type(exc).__name__}: {exc}'
else:
logger.error('Job %s timed out after %ds', job.id, _effective_timeout(timeout))
job.status = 'failed'
job.error = f'Job timed out after {_effective_timeout(timeout)}s'
except Exception as exc:
tb = traceback.format_exc()
logger.error('Job %s failed: %s\n%s', job.id, exc, tb)
Expand Down
2 changes: 1 addition & 1 deletion murphy/api/request_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class ExecuteRequest(BaseModel):
judge_provider: str | None = None
judge_model: str | None = None
max_steps: int = 15
max_concurrent: int = 3
max_concurrent: int = 1
webhook_url: str | None = None
async_mode: bool = Field(False, alias='async')

Expand Down
14 changes: 13 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "murphy"
description = "AI-driven website evaluation powered by browser-use"
authors = [{ name = "MIH AI B.V." }]
version = "1.1.0"
version = "1.1.3"
readme = "README.md"
requires-python = ">=3.11,<4.0"
classifiers = [
Expand Down Expand Up @@ -210,3 +210,15 @@ dev-dependencies = [
"pytest-timeout>=2.4.0",
"pytest-cov>=7.0.0",
]

# Pull torch from the PyTorch CPU-only index. The default PyPI wheel bundles
# ~2.5 GB of unused NVIDIA CUDA libraries — Fargate has no GPU and torch falls
# back to CPU mode either way, so the CUDA payload is dead weight that bloats
# the container image and slows cold starts.
[tool.uv.sources]
torch = { index = "pytorch-cpu" }

[[tool.uv.index]]
name = "pytorch-cpu"
url = "https://download.pytorch.org/whl/cpu"
explicit = true
19 changes: 19 additions & 0 deletions tests/browser_use/browser/test_local_browser_watchdog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from browser_use.browser.watchdogs.local_browser_watchdog import cdp_ready_timeout


def test_cdp_ready_timeout_defaults_to_180(monkeypatch):
monkeypatch.delenv('TIMEOUT_BrowserCDPReady', raising=False)

assert cdp_ready_timeout() == 180.0


def test_cdp_ready_timeout_uses_env_override(monkeypatch):
monkeypatch.setenv('TIMEOUT_BrowserCDPReady', '45.5')

assert cdp_ready_timeout() == 45.5


def test_cdp_ready_timeout_ignores_invalid_env_override(monkeypatch):
monkeypatch.setenv('TIMEOUT_BrowserCDPReady', 'not-a-number')

assert cdp_ready_timeout() == 180.0
13 changes: 13 additions & 0 deletions tests/murphy/api/test_jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,19 @@ async def slow_fn(req):
assert job.error is not None and 'timed out' in job.error


@pytest.mark.asyncio
async def test_execute_with_semaphore_preserves_inner_timeout(monkeypatch):
monkeypatch.setattr('murphy.api.jobs.MURPHY_JOB_TIMEOUT_OVERRIDE', None)

async def browser_start_fn(req):
raise TimeoutError('Browser did not start within 180 seconds')

job = Job(id='browser-timeout')
await _execute_with_semaphore(job, browser_start_fn, {}, timeout=1800)
assert job.status == 'failed'
assert job.error == 'TimeoutError: Browser did not start within 180 seconds'


@pytest.mark.asyncio
async def test_execute_with_semaphore_exception(monkeypatch):
monkeypatch.setattr('murphy.api.jobs.MURPHY_JOB_TIMEOUT_OVERRIDE', None)
Expand Down
2 changes: 1 addition & 1 deletion tests/murphy/api/test_request_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ def test_execute_request_defaults():
assert r.test_plan is None
assert r.evaluate_job_id is None
assert r.max_steps == 15
assert r.max_concurrent == 3
assert r.max_concurrent == 1


def test_execute_request_with_json_string_test_plan():
Expand Down
Loading
Loading