From cad8abe03b1a4375c666a3d6574d80b3fdf7a945 Mon Sep 17 00:00:00 2001 From: tnsardesai <18272584+tnsardesai@users.noreply.github.com> Date: Tue, 18 Aug 2026 01:40:33 +0000 Subject: [PATCH 1/3] 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. --- src/kernel/lib/browser_routing/routing.py | 2 +- tests/test_browser_routing.py | 86 ++++++++++++++++++++++- 2 files changed, 84 insertions(+), 4 deletions(-) diff --git a/src/kernel/lib/browser_routing/routing.py b/src/kernel/lib/browser_routing/routing.py index 7f28d726..11cd7647 100644 --- a/src/kernel/lib/browser_routing/routing.py +++ b/src/kernel/lib/browser_routing/routing.py @@ -44,7 +44,7 @@ def browser_routing_config_from_env() -> BrowserRoutingConfig: # Path prefixes eligible for direct-to-VM routing. "telemetry/stream" is # the live SSE endpoint (VM); "telemetry/events" is a historical read # served by the control plane (S2) and must NOT be here. - return BrowserRoutingConfig(subresources=("curl", "telemetry/stream")) + return BrowserRoutingConfig(subresources=("curl", "telemetry/stream", "computer", "playwright")) if raw.strip() == "": return BrowserRoutingConfig() diff --git a/tests/test_browser_routing.py b/tests/test_browser_routing.py index 3538c221..51f23a1b 100644 --- a/tests/test_browser_routing.py +++ b/tests/test_browser_routing.py @@ -392,7 +392,12 @@ def test_browser_route_from_browser_requires_base_url_and_jwt() -> None: def test_browser_routing_config_from_env_defaults_to_curl(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False) - assert browser_routing_config_from_env().subresources == ("curl", "telemetry/stream") + assert browser_routing_config_from_env().subresources == ( + "curl", + "telemetry/stream", + "computer", + "playwright", + ) def test_direct_vm_routing_allowlist_segment_boundary() -> None: @@ -401,13 +406,16 @@ def test_direct_vm_routing_allowlist_segment_boundary() -> None: # stream-prefixed-but-different path is not matched. from kernel.lib.browser_routing.routing import _matches_direct_vm_prefix - prefixes = ("curl", "telemetry/stream") + prefixes = ("curl", "telemetry/stream", "computer", "playwright") assert _matches_direct_vm_prefix("telemetry/stream", prefixes) is True assert _matches_direct_vm_prefix("telemetry/stream/x", prefixes) is True assert _matches_direct_vm_prefix("telemetry/events", prefixes) is False assert _matches_direct_vm_prefix("telemetry/streaming-config", prefixes) is False assert _matches_direct_vm_prefix("telemetry", prefixes) is False assert _matches_direct_vm_prefix("curl/raw", prefixes) is True + assert _matches_direct_vm_prefix("computer/screenshot", prefixes) is True + assert _matches_direct_vm_prefix("playwright/execute", prefixes) is True + assert _matches_direct_vm_prefix("process/exec", prefixes) is False assert _matches_direct_vm_prefix("fs/read", prefixes) is False @@ -427,7 +435,7 @@ def test_rewrite_direct_vm_options_keeps_telemetry_events_on_control_plane() -> cache.set( BrowserRoute(session_id="sess-1", base_url="http://browser-session.test/browser/kernel", jwt="token-abc") ) - config = BrowserRoutingConfig(subresources=("curl", "telemetry/stream")) + config = BrowserRoutingConfig(subresources=("curl", "telemetry/stream", "computer", "playwright")) events = rewrite_direct_vm_options( 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() -> ) assert str(stream.url).startswith("http://browser-session.test/browser/kernel/telemetry/stream") + screenshot = rewrite_direct_vm_options( + FinalRequestOptions(method="post", url="/browsers/sess-1/computer/screenshot"), cache=cache, config=config + ) + assert str(screenshot.url).startswith("http://browser-session.test/browser/kernel/computer/screenshot") + + execute = rewrite_direct_vm_options( + FinalRequestOptions(method="post", url="/browsers/sess-1/playwright/execute"), cache=cache, config=config + ) + assert str(execute.url).startswith("http://browser-session.test/browser/kernel/playwright/execute") + + process = rewrite_direct_vm_options( + FinalRequestOptions(method="post", url="/browsers/sess-1/process/exec"), cache=cache, config=config + ) + assert process.url == "/browsers/sess-1/process/exec" + + fs_read = rewrite_direct_vm_options( + FinalRequestOptions(method="get", url="/browsers/sess-1/fs/read_file"), cache=cache, config=config + ) + assert fs_read.url == "/browsers/sess-1/fs/read_file" + def test_browser_routing_config_from_env_empty_string_disables_routing(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", "") assert browser_routing_config_from_env().subresources == () + + +@respx.mock +def test_computer_screenshot_and_playwright_execute_route_to_vm_by_default( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False) + screenshot = respx.post("http://browser-session.test/browser/kernel/computer/screenshot").mock( + return_value=httpx.Response(200, content=b"png", headers={"content-type": "image/png"}) + ) + execute = respx.post("http://browser-session.test/browser/kernel/playwright/execute").mock( + return_value=httpx.Response(200, json={"success": True}) + ) + with Kernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) as client: + _cache_browser(client) + client.browsers.computer.capture_screenshot("sess-1") + out = client.browsers.playwright.execute("sess-1", code="return 1") + + assert screenshot.called + screenshot_req = cast(httpx.Request, cast(Any, screenshot.calls[0]).request) + assert screenshot_req.url.params.get("jwt") == "token-abc" + assert screenshot_req.headers.get("Authorization") is None + assert execute.called + execute_req = cast(httpx.Request, cast(Any, execute.calls[0]).request) + assert execute_req.url.params.get("jwt") == "token-abc" + assert execute_req.headers.get("Authorization") is None + assert out.success is True + + +@respx.mock +def test_process_fs_and_telemetry_events_stay_on_api_origin_by_default( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False) + process = respx.post(f"{base_url}/browsers/sess-1/process/exec").mock( + return_value=httpx.Response(200, json={"exit_code": 0, "stdout_b64": "", "stderr_b64": ""}) + ) + fs_read = respx.get(f"{base_url}/browsers/sess-1/fs/read_file").mock( + return_value=httpx.Response(200, content=b"x", headers={"content-type": "application/octet-stream"}) + ) + events = respx.get(f"{base_url}/browsers/sess-1/telemetry/events").mock( + return_value=httpx.Response(200, json=[]) + ) + with Kernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) as client: + _cache_browser(client) + client.browsers.process.exec("sess-1", command="echo") + client.browsers.fs.read_file("sess-1", path="/tmp/x") + client.browsers.telemetry.events("sess-1") + + assert process.called + assert fs_read.called + assert events.called From b32fc1d18419d02119759c1cd0533350b533a8a9 Mon Sep 17 00:00:00 2001 From: tnsardesai <18272584+tnsardesai@users.noreply.github.com> Date: Tue, 18 Aug 2026 20:44:23 +0000 Subject: [PATCH 2/3] Rename default-allowlist test to match the new list The default now includes computer and playwright, not just curl. --- tests/test_browser_routing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_browser_routing.py b/tests/test_browser_routing.py index 51f23a1b..4921a5be 100644 --- a/tests/test_browser_routing.py +++ b/tests/test_browser_routing.py @@ -390,7 +390,7 @@ def test_browser_route_from_browser_requires_base_url_and_jwt() -> None: assert browser_route_from_browser({**_fake_browser(), "cdp_ws_url": None}) is None -def test_browser_routing_config_from_env_defaults_to_curl(monkeypatch: pytest.MonkeyPatch) -> None: +def test_browser_routing_config_from_env_defaults_to_curl_telemetry_computer_and_playwright(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False) assert browser_routing_config_from_env().subresources == ( "curl", From 1b28d622902bb71cd3c0303e0a9fd0066918e773 Mon Sep 17 00:00:00 2001 From: tnsardesai <18272584+tnsardesai@users.noreply.github.com> Date: Tue, 18 Aug 2026 21:02:51 +0000 Subject: [PATCH 3/3] Shorten default-allowlist test name The asserted list can grow; the test name should not. --- tests/test_browser_routing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_browser_routing.py b/tests/test_browser_routing.py index 4921a5be..0f43f716 100644 --- a/tests/test_browser_routing.py +++ b/tests/test_browser_routing.py @@ -390,7 +390,7 @@ def test_browser_route_from_browser_requires_base_url_and_jwt() -> None: assert browser_route_from_browser({**_fake_browser(), "cdp_ws_url": None}) is None -def test_browser_routing_config_from_env_defaults_to_curl_telemetry_computer_and_playwright(monkeypatch: pytest.MonkeyPatch) -> None: +def test_browser_routing_config_from_env_defaults(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False) assert browser_routing_config_from_env().subresources == ( "curl",