Skip to content

Commit cad8abe

Browse files
committed
Route computer and playwright calls directly to the VM
Add computer and playwright to the default browser routing allowlist so screenshot and execute traffic can skip the control plane once metro-api records activity on the kernel proxy.
1 parent 20d3bba commit cad8abe

2 files changed

Lines changed: 84 additions & 4 deletions

File tree

src/kernel/lib/browser_routing/routing.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def browser_routing_config_from_env() -> BrowserRoutingConfig:
4444
# Path prefixes eligible for direct-to-VM routing. "telemetry/stream" is
4545
# the live SSE endpoint (VM); "telemetry/events" is a historical read
4646
# served by the control plane (S2) and must NOT be here.
47-
return BrowserRoutingConfig(subresources=("curl", "telemetry/stream"))
47+
return BrowserRoutingConfig(subresources=("curl", "telemetry/stream", "computer", "playwright"))
4848
if raw.strip() == "":
4949
return BrowserRoutingConfig()
5050

tests/test_browser_routing.py

Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,12 @@ def test_browser_route_from_browser_requires_base_url_and_jwt() -> None:
392392

393393
def test_browser_routing_config_from_env_defaults_to_curl(monkeypatch: pytest.MonkeyPatch) -> None:
394394
monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
395-
assert browser_routing_config_from_env().subresources == ("curl", "telemetry/stream")
395+
assert browser_routing_config_from_env().subresources == (
396+
"curl",
397+
"telemetry/stream",
398+
"computer",
399+
"playwright",
400+
)
396401

397402

398403
def test_direct_vm_routing_allowlist_segment_boundary() -> None:
@@ -401,13 +406,16 @@ def test_direct_vm_routing_allowlist_segment_boundary() -> None:
401406
# stream-prefixed-but-different path is not matched.
402407
from kernel.lib.browser_routing.routing import _matches_direct_vm_prefix
403408

404-
prefixes = ("curl", "telemetry/stream")
409+
prefixes = ("curl", "telemetry/stream", "computer", "playwright")
405410
assert _matches_direct_vm_prefix("telemetry/stream", prefixes) is True
406411
assert _matches_direct_vm_prefix("telemetry/stream/x", prefixes) is True
407412
assert _matches_direct_vm_prefix("telemetry/events", prefixes) is False
408413
assert _matches_direct_vm_prefix("telemetry/streaming-config", prefixes) is False
409414
assert _matches_direct_vm_prefix("telemetry", prefixes) is False
410415
assert _matches_direct_vm_prefix("curl/raw", prefixes) is True
416+
assert _matches_direct_vm_prefix("computer/screenshot", prefixes) is True
417+
assert _matches_direct_vm_prefix("playwright/execute", prefixes) is True
418+
assert _matches_direct_vm_prefix("process/exec", prefixes) is False
411419
assert _matches_direct_vm_prefix("fs/read", prefixes) is False
412420

413421

@@ -427,7 +435,7 @@ def test_rewrite_direct_vm_options_keeps_telemetry_events_on_control_plane() ->
427435
cache.set(
428436
BrowserRoute(session_id="sess-1", base_url="http://browser-session.test/browser/kernel", jwt="token-abc")
429437
)
430-
config = BrowserRoutingConfig(subresources=("curl", "telemetry/stream"))
438+
config = BrowserRoutingConfig(subresources=("curl", "telemetry/stream", "computer", "playwright"))
431439

432440
events = rewrite_direct_vm_options(
433441
FinalRequestOptions(method="get", url="/browsers/sess-1/telemetry/events"), cache=cache, config=config
@@ -439,7 +447,79 @@ def test_rewrite_direct_vm_options_keeps_telemetry_events_on_control_plane() ->
439447
)
440448
assert str(stream.url).startswith("http://browser-session.test/browser/kernel/telemetry/stream")
441449

450+
screenshot = rewrite_direct_vm_options(
451+
FinalRequestOptions(method="post", url="/browsers/sess-1/computer/screenshot"), cache=cache, config=config
452+
)
453+
assert str(screenshot.url).startswith("http://browser-session.test/browser/kernel/computer/screenshot")
454+
455+
execute = rewrite_direct_vm_options(
456+
FinalRequestOptions(method="post", url="/browsers/sess-1/playwright/execute"), cache=cache, config=config
457+
)
458+
assert str(execute.url).startswith("http://browser-session.test/browser/kernel/playwright/execute")
459+
460+
process = rewrite_direct_vm_options(
461+
FinalRequestOptions(method="post", url="/browsers/sess-1/process/exec"), cache=cache, config=config
462+
)
463+
assert process.url == "/browsers/sess-1/process/exec"
464+
465+
fs_read = rewrite_direct_vm_options(
466+
FinalRequestOptions(method="get", url="/browsers/sess-1/fs/read_file"), cache=cache, config=config
467+
)
468+
assert fs_read.url == "/browsers/sess-1/fs/read_file"
469+
442470

443471
def test_browser_routing_config_from_env_empty_string_disables_routing(monkeypatch: pytest.MonkeyPatch) -> None:
444472
monkeypatch.setenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", "")
445473
assert browser_routing_config_from_env().subresources == ()
474+
475+
476+
@respx.mock
477+
def test_computer_screenshot_and_playwright_execute_route_to_vm_by_default(
478+
monkeypatch: pytest.MonkeyPatch,
479+
) -> None:
480+
monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
481+
screenshot = respx.post("http://browser-session.test/browser/kernel/computer/screenshot").mock(
482+
return_value=httpx.Response(200, content=b"png", headers={"content-type": "image/png"})
483+
)
484+
execute = respx.post("http://browser-session.test/browser/kernel/playwright/execute").mock(
485+
return_value=httpx.Response(200, json={"success": True})
486+
)
487+
with Kernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) as client:
488+
_cache_browser(client)
489+
client.browsers.computer.capture_screenshot("sess-1")
490+
out = client.browsers.playwright.execute("sess-1", code="return 1")
491+
492+
assert screenshot.called
493+
screenshot_req = cast(httpx.Request, cast(Any, screenshot.calls[0]).request)
494+
assert screenshot_req.url.params.get("jwt") == "token-abc"
495+
assert screenshot_req.headers.get("Authorization") is None
496+
assert execute.called
497+
execute_req = cast(httpx.Request, cast(Any, execute.calls[0]).request)
498+
assert execute_req.url.params.get("jwt") == "token-abc"
499+
assert execute_req.headers.get("Authorization") is None
500+
assert out.success is True
501+
502+
503+
@respx.mock
504+
def test_process_fs_and_telemetry_events_stay_on_api_origin_by_default(
505+
monkeypatch: pytest.MonkeyPatch,
506+
) -> None:
507+
monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
508+
process = respx.post(f"{base_url}/browsers/sess-1/process/exec").mock(
509+
return_value=httpx.Response(200, json={"exit_code": 0, "stdout_b64": "", "stderr_b64": ""})
510+
)
511+
fs_read = respx.get(f"{base_url}/browsers/sess-1/fs/read_file").mock(
512+
return_value=httpx.Response(200, content=b"x", headers={"content-type": "application/octet-stream"})
513+
)
514+
events = respx.get(f"{base_url}/browsers/sess-1/telemetry/events").mock(
515+
return_value=httpx.Response(200, json=[])
516+
)
517+
with Kernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) as client:
518+
_cache_browser(client)
519+
client.browsers.process.exec("sess-1", command="echo")
520+
client.browsers.fs.read_file("sess-1", path="/tmp/x")
521+
client.browsers.telemetry.events("sess-1")
522+
523+
assert process.called
524+
assert fs_read.called
525+
assert events.called

0 commit comments

Comments
 (0)