fix(browser): gate lightpanda --http-cache-dir on binary support

Salvage follow-up for PR #100269. The flag landed upstream in 0.3.x;
binaries before it (e.g. 0.2.8, verified locally) fatally reject the
flag with 'unknown argument', breaking every Browser Use launch.
Probe 'lightpanda help' once per process and omit the flag when the
binary predates it. Also soften the unverified concurrency claim in
the _http_cache_dir docstring and tell docs readers to stop sessions
before deleting the live sqlite cache.
This commit is contained in:
kshitijk4poor
2026-09-03 02:30:43 +05:30
committed by kshitij
parent 6973ce3ac2
commit eac466d7bd
3 changed files with 69 additions and 8 deletions

View File

@@ -43,6 +43,7 @@ def _isolate(tmp_path, monkeypatch):
# Never touch the developer's real ~/.local/bin/lightpanda.
monkeypatch.setattr(lp, "_home_candidates", lambda: [])
monkeypatch.setattr(lp, "_safe_start_time", lambda pid: 111)
monkeypatch.setattr(lp, "_supports_http_cache", True)
with lp._servers_lock:
lp._servers.clear()
yield state
@@ -142,6 +143,39 @@ class TestLaunch:
assert Path(cache).is_dir()
assert not list(Path(cache).glob("*.json")) # never confused with a session record
def test_no_http_cache_flag_on_old_binary(self, monkeypatch, _isolate):
monkeypatch.setattr(lp, "_supports_http_cache", False)
_, err, calls = self._launch(monkeypatch)
assert err is None
assert "--http-cache-dir" not in calls["argv"]
assert calls["argv"][-1] == "43111"
def test_http_cache_probe_caches_and_detects_flag(self, monkeypatch, tmp_path):
exe = _exe(tmp_path / "lightpanda")
runs = []
class FakeRun:
def __init__(self, stdout):
self.stdout = stdout
def fake_run(argv, **kwargs):
runs.append(argv)
return FakeRun("--http-cache-dir <PATH>" if len(runs) == 1 else "")
monkeypatch.setattr(lp, "_supports_http_cache", None)
monkeypatch.setattr(lp.subprocess, "run", fake_run)
assert lp._binary_supports_http_cache(str(exe)) is True
assert lp._binary_supports_http_cache(str(exe)) is True # cached, single probe
assert len(runs) == 1
monkeypatch.setattr(lp, "_supports_http_cache", None)
def fake_run_old(argv, **kwargs):
return FakeRun("no such flag here")
monkeypatch.setattr(lp.subprocess, "run", fake_run_old)
assert lp._binary_supports_http_cache(str(exe)) is False
def test_block_private_networks_flag(self, monkeypatch):
_, err, calls = self._launch(monkeypatch, block_private_networks=True)
assert err is None

View File

@@ -119,15 +119,43 @@ def _http_cache_dir() -> Path:
"""Filesystem HTTP cache shared by every Lightpanda this Hermes spawns.
Shared rather than per-session so a cached asset survives session churn.
Lightpanda holds it in sqlite (WAL); a write that loses a race is a cache
miss, never a failed page load, and ``--http-cache-entry-limit`` (default
1000) bounds it without Hermes managing eviction.
Lightpanda holds it in sqlite (WAL) with a best-effort write path, and
``--http-cache-entry-limit`` (upstream default 1000, not passed here)
bounds it without Hermes managing eviction.
"""
path = _state_dir() / "http-cache"
path.mkdir(parents=True, exist_ok=True)
return path
_supports_http_cache: Optional[bool] = None
def _binary_supports_http_cache(binary: str) -> bool:
"""True if ``lightpanda serve`` accepts ``--http-cache-dir``.
The flag landed upstream in 0.3.x; older binaries fatally reject it
("unknown argument"), which would break every launch. Probing ``help``
output keeps working across future flag additions without parsing
versions, and is cached for the process lifetime.
"""
global _supports_http_cache
if _supports_http_cache is None:
try:
out = subprocess.run(
[binary, "help"], capture_output=True, text=True, timeout=10
)
_supports_http_cache = "--http-cache-dir" in out.stdout
except Exception as e:
logger.debug("lightpanda http-cache probe failed (%s); assuming no", e)
_supports_http_cache = False
if not _supports_http_cache:
logger.debug(
"lightpanda %s predates --http-cache-dir; serving without a cache", binary
)
return _supports_http_cache
def _record_path(session_name: str) -> Path:
return _state_dir() / f"{session_name}.json"
@@ -229,10 +257,9 @@ def launch_lightpanda(
)
port = _pick_free_loopback_port()
argv = [
binary, "serve", "--host", "127.0.0.1", "--port", str(port),
"--http-cache-dir", str(_http_cache_dir()),
]
argv = [binary, "serve", "--host", "127.0.0.1", "--port", str(port)]
if _binary_supports_http_cache(binary):
argv += ["--http-cache-dir", str(_http_cache_dir())]
if block_private_networks:
argv.append("--block-private-networks")
log_path = str(_state_dir() / f"{session_name}.log")

View File

@@ -455,7 +455,7 @@ AGENT_BROWSER_ENGINE=lightpanda
The engine works with both browser drivers:
- **Browser Use mode (the default).** Hermes launches `lightpanda serve --host 127.0.0.1 --port <free>` itself — one process per `browser_exec` session name (or per task) — and points the Browser Use CLI at it. No Chromium, Playwright or Node.js is needed. The process is reaped after `browser.inactivity_timeout`, on exit, and by the orphan sweep if Hermes crashes. All of these processes share one on-disk HTTP cache at `$HERMES_HOME/cache/browser-use/lightpanda/http-cache`, so repeat visits skip re-downloading assets; delete that directory to clear it. Lightpanda has no graphical renderer, so `capture_screenshot()` is unavailable and the tool description tells the model to work text-first; it also holds one page per session, so the model is told to call `new_tab()` once and `goto_url()` afterwards (tracked upstream in [lightpanda-io/browser#1962](https://github.com/lightpanda-io/browser/issues/1962)).
- **Browser Use mode (the default).** Hermes launches `lightpanda serve --host 127.0.0.1 --port <free>` itself — one process per `browser_exec` session name (or per task) — and points the Browser Use CLI at it. No Chromium, Playwright or Node.js is needed. The process is reaped after `browser.inactivity_timeout`, on exit, and by the orphan sweep if Hermes crashes. All of these processes share one on-disk HTTP cache at `$HERMES_HOME/cache/browser-use/lightpanda/http-cache`, so repeat visits skip re-downloading assets. Hermes passes the cache flag only when the installed Lightpanda supports it (0.3.x+); older binaries simply run without a cache. To clear it, stop your Lightpanda sessions first, then delete that directory. Lightpanda has no graphical renderer, so `capture_screenshot()` is unavailable and the tool description tells the model to work text-first; it also holds one page per session, so the model is told to call `new_tab()` once and `goto_url()` afterwards (tracked upstream in [lightpanda-io/browser#1962](https://github.com/lightpanda-io/browser/issues/1962)).
- **Built-in browser tools** (`/browser use off`). Hermes drives Lightpanda through `agent-browser --engine lightpanda` over CDP, the same way it drives local Chrome, with **automatic Chrome fallback**: Lightpanda handles the actions it supports (navigate, snapshot, click, type, scroll, back, press, eval) and Hermes transparently retries on Chrome for anything it doesn't. Screenshots and `browser_vision` are routed straight to Chrome.
**When the engine is ignored.** `browser.engine` is the lowest-precedence browser setting: a cloud provider (including the Nous subscription browser — and on never-configured setups, any `BROWSERBASE_API_KEY` / `BROWSER_USE_API_KEY` in `~/.hermes/.env` auto-selects one), Camofox, a `browser.cdp_url` / `/browser connect` override, or `browser.use_real_profile` all take precedence. Picking Lightpanda in `hermes tools` writes `cloud_provider: local` for you; `/browser status` and `hermes doctor` report when the engine is configured but shadowed, and by what.