Skip to content

Commit 8cabe65

Browse files
committed
Harden Kernel replay finalization
1 parent dcbe707 commit 8cabe65

3 files changed

Lines changed: 73 additions & 15 deletions

File tree

src/clawbench/runner/batch.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ def detect_engine() -> str:
4949
"claw-eval": "test-cases/claw-eval",
5050
}
5151
DEFAULT_CASES_SUITE = "v2"
52+
MANAGED_BROWSER_RUNTIMES = frozenset({"browserbase", "kernel"})
5253

5354

5455
def load_models_yaml() -> dict:
@@ -289,7 +290,7 @@ async def run_job(
289290
cmd_parts += ["--harness", harness]
290291
if browser_runtime:
291292
cmd_parts += ["--browser-runtime", browser_runtime]
292-
if browser_runtime == "kernel":
293+
if browser_runtime in MANAGED_BROWSER_RUNTIMES:
293294
cmd_parts.append("--hide-browser-viewer")
294295
if browser_cdp_url:
295296
cmd_parts += ["--browser-cdp-url", browser_cdp_url]
@@ -591,11 +592,10 @@ async def async_main(args: argparse.Namespace) -> int:
591592
shutdown_event = asyncio.Event()
592593
running_procs.clear()
593594
browser_runtime = getattr(args, "browser_runtime", None) or "local"
594-
managed_browser_runtimes = {"browserbase", "kernel"}
595595
if getattr(args, "max_concurrent", None) is None:
596-
args.max_concurrent = 1 if browser_runtime in managed_browser_runtimes else 2
596+
args.max_concurrent = 1 if browser_runtime in MANAGED_BROWSER_RUNTIMES else 2
597597
if (
598-
browser_runtime in managed_browser_runtimes
598+
browser_runtime in MANAGED_BROWSER_RUNTIMES
599599
and args.harness == "claude-code-chrome-extension"
600600
):
601601
print(

src/clawbench/runner/run_support/browser_runtime/providers.py

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -442,7 +442,7 @@ def __init__(
442442
options: dict[str, Any],
443443
api_url: str = _KERNEL_API_URL,
444444
replay_poll_interval_s: float = 1,
445-
replay_poll_timeout_s: float = 30,
445+
replay_poll_timeout_s: float = 300,
446446
) -> None:
447447
unknown = sorted(set(options) - _KERNEL_ALLOWED_OPTIONS)
448448
if unknown:
@@ -471,14 +471,14 @@ def __init__(
471471
self.replay_poll_interval_s = replay_poll_interval_s
472472
self.replay_poll_timeout_s = replay_poll_timeout_s
473473

474-
def _request(
474+
def _request_response(
475475
self,
476476
method: str,
477477
path: str,
478478
payload: dict[str, Any] | None = None,
479479
*,
480480
accept: str = "application/json",
481-
) -> bytes:
481+
) -> tuple[int, Any, bytes]:
482482
assert self.api_key is not None
483483
data = (
484484
json.dumps(payload, separators=(",", ":")).encode()
@@ -497,7 +497,11 @@ def _request(
497497
)
498498
try:
499499
with urllib.request.urlopen(request, timeout=30) as response:
500-
return response.read()
500+
return (
501+
int(getattr(response, "status", 200)),
502+
getattr(response, "headers", {}),
503+
response.read(),
504+
)
501505
except urllib.error.HTTPError as e:
502506
if e.code in {401, 403}:
503507
message = "Kernel authentication failed"
@@ -517,6 +521,22 @@ def _request(
517521
reason = reason.replace(secret, "[REDACTED]")
518522
raise _KernelApiError(f"Kernel API request failed: {reason}") from None
519523

524+
def _request(
525+
self,
526+
method: str,
527+
path: str,
528+
payload: dict[str, Any] | None = None,
529+
*,
530+
accept: str = "application/json",
531+
) -> bytes:
532+
_status, _headers, raw = self._request_response(
533+
method,
534+
path,
535+
payload,
536+
accept=accept,
537+
)
538+
return raw
539+
520540
def _request_json(
521541
self,
522542
method: str,
@@ -669,11 +689,31 @@ def finalize(self, session: BrowserSession, output_dir: Path) -> None:
669689
)
670690
time.sleep(self.replay_poll_interval_s)
671691

672-
recording = self._request(
673-
"GET",
674-
f"/browsers/{session.session_id}/replays/{replay_id}",
675-
accept="video/mp4",
676-
)
692+
while True:
693+
status, headers, recording = self._request_response(
694+
"GET",
695+
f"/browsers/{session.session_id}/replays/{replay_id}",
696+
accept="video/mp4",
697+
)
698+
if status != 202:
699+
break
700+
if time.monotonic() >= deadline:
701+
raise _KernelApiError(
702+
"Kernel replay did not finish downloading before timeout"
703+
)
704+
retry_after: str | None = (
705+
headers.get("Retry-After") if headers else None
706+
)
707+
if retry_after is None:
708+
delay = self.replay_poll_interval_s
709+
else:
710+
try:
711+
delay = float(retry_after)
712+
except ValueError:
713+
delay = self.replay_poll_interval_s
714+
time.sleep(min(max(0, delay), max(0, deadline - time.monotonic())))
715+
if status != 200:
716+
raise _KernelApiError(f"Kernel replay download returned HTTP {status}")
677717
if not recording:
678718
raise _KernelApiError("Kernel replay download was empty")
679719
recording_path = output_dir / "data" / "recording.mp4"

tests/test_browser_runtime.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,16 @@
2626

2727

2828
class _FakeResponse:
29-
def __init__(self, payload: object) -> None:
29+
def __init__(
30+
self,
31+
payload: object,
32+
*,
33+
status: int = 200,
34+
headers: dict[str, str] | None = None,
35+
) -> None:
3036
self.payload = payload
37+
self.status = status
38+
self.headers = headers or {}
3139

3240
def __enter__(self) -> _FakeResponse:
3341
return self
@@ -322,11 +330,13 @@ def test_kernel_session_replay_and_cleanup(
322330
tmp_path: Path,
323331
) -> None:
324332
calls: list[tuple[str, str, object, str | None]] = []
333+
replay_downloads = 0
325334

326335
def fake_urlopen(
327336
request: urllib.request.Request,
328337
timeout: int,
329338
) -> _FakeResponse:
339+
nonlocal replay_downloads
330340
assert timeout == 30
331341
if request.data:
332342
assert isinstance(request.data, bytes)
@@ -373,6 +383,13 @@ def fake_urlopen(
373383
if request.full_url.endswith("/replays/replay_123/stop"):
374384
return _FakeResponse(b"")
375385
if request.full_url.endswith("/replays/replay_123"):
386+
replay_downloads += 1
387+
if replay_downloads == 1:
388+
return _FakeResponse(
389+
b"not-ready",
390+
status=202,
391+
headers={"Retry-After": "0"},
392+
)
376393
return _FakeResponse(b"mp4-data")
377394
if request.full_url.endswith("/browsers/browser_123"):
378395
return _FakeResponse(b"")
@@ -383,7 +400,7 @@ def fake_urlopen(
383400
api_key="kernel-secret",
384401
options={"stealth": True, "region": "us-east"},
385402
replay_poll_interval_s=0,
386-
replay_poll_timeout_s=0,
403+
replay_poll_timeout_s=1,
387404
)
388405

389406
session = provider.start({}, 1800)
@@ -401,6 +418,7 @@ def fake_urlopen(
401418
assert session.provider == "kernel"
402419
assert session.recording_mode == "provider-download"
403420
assert session.cleanup_status == "deleted"
421+
assert replay_downloads == 2
404422
assert (tmp_path / "data" / "recording.mp4").read_bytes() == b"mp4-data"
405423
metadata = json.dumps(session.to_metadata())
406424
assert "cdp-secret" not in metadata

0 commit comments

Comments
 (0)