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/7] 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/7] 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/7] 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", From f525a242bda54cec7301eae0dd3e12dd3c9b4e33 Mon Sep 17 00:00:00 2001 From: tnsardesai <18272584+tnsardesai@users.noreply.github.com> Date: Fri, 21 Aug 2026 18:04:42 +0000 Subject: [PATCH 4/7] Fall back to the control plane on a stale session JWT A direct-to-VM 401/403 now evicts the cached route and retries the original request against the API. --- src/kernel/_client.py | 15 +++++++++++++ src/kernel/lib/browser_routing/routing.py | 26 +++++++++++++++++++---- tests/test_browser_routing.py | 23 ++++++++++++++++++++ 3 files changed, 60 insertions(+), 4 deletions(-) diff --git a/src/kernel/_client.py b/src/kernel/_client.py index 497cc5d5..d41fa974 100644 --- a/src/kernel/_client.py +++ b/src/kernel/_client.py @@ -42,6 +42,7 @@ rewrite_direct_vm_options, browser_routing_config_from_env, maybe_evict_browser_route_from_response, + should_retry_stale_direct_vm_auth, maybe_populate_browser_route_cache_from_response, ) @@ -353,6 +354,13 @@ def _prepare_options(self, options: Any) -> Any: def _prepare_request(self, request: httpx.Request) -> None: strip_direct_vm_auth(request, cache=self.browser_route_cache) + @override + def _should_retry(self, response: httpx.Response) -> bool: + if should_retry_stale_direct_vm_auth(response, cache=self.browser_route_cache): + maybe_evict_browser_route_from_response(response, cache=self.browser_route_cache) + return True + return super()._should_retry(response) + @override def _process_response( self, @@ -722,6 +730,13 @@ async def _prepare_options(self, options: Any) -> Any: async def _prepare_request(self, request: httpx.Request) -> None: strip_direct_vm_auth(request, cache=self.browser_route_cache) + @override + def _should_retry(self, response: httpx.Response) -> bool: + if should_retry_stale_direct_vm_auth(response, cache=self.browser_route_cache): + maybe_evict_browser_route_from_response(response, cache=self.browser_route_cache) + return True + return super()._should_retry(response) + @override async def _process_response( self, diff --git a/src/kernel/lib/browser_routing/routing.py b/src/kernel/lib/browser_routing/routing.py index 11cd7647..31d8218a 100644 --- a/src/kernel/lib/browser_routing/routing.py +++ b/src/kernel/lib/browser_routing/routing.py @@ -110,14 +110,18 @@ def maybe_populate_browser_route_cache_from_response(response: httpx.Response, * def maybe_evict_browser_route_from_response(response: httpx.Response, *, cache: BrowserRouteCache) -> None: - if not response.is_success: + if response.is_success: + session_id = _session_id_to_evict_from_response(response) + if session_id: + cache.delete(session_id) return - session_id = _session_id_to_evict_from_response(response) - if not session_id: + if response.status_code not in {401, 403}: return - cache.delete(session_id) + session_id = _session_id_from_direct_vm_response(response, cache=cache) + if session_id: + cache.delete(session_id) def populate_browser_route_cache_from_value(value: object, *, cache: BrowserRouteCache) -> None: @@ -161,6 +165,20 @@ def _session_id_to_evict_from_response(response: httpx.Response) -> str | None: return None +def _session_id_from_direct_vm_response(response: httpx.Response, *, cache: BrowserRouteCache) -> str | None: + raw = str(response.request.url) + for route in cache.values(): + if raw.startswith(route.base_url.rstrip("/") + "/"): + return route.session_id + return None + + +def should_retry_stale_direct_vm_auth(response: httpx.Response, *, cache: BrowserRouteCache) -> bool: + if response.status_code not in {401, 403}: + return False + return _session_id_from_direct_vm_response(response, cache=cache) is not None + + def _session_id_from_browser_delete_path(path: str) -> str | None: match = _BROWSER_DELETE_BY_ID_PATH.match(path) if match is None: diff --git a/tests/test_browser_routing.py b/tests/test_browser_routing.py index 0f43f716..d4997d35 100644 --- a/tests/test_browser_routing.py +++ b/tests/test_browser_routing.py @@ -523,3 +523,26 @@ def test_process_fs_and_telemetry_events_stay_on_api_origin_by_default( assert process.called assert fs_read.called assert events.called + + +@respx.mock +def test_stale_direct_vm_jwt_evicts_cache_and_retries_control_plane( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False) + monkeypatch.setattr("kernel._base_client.SyncAPIClient._sleep_for_retry", lambda *args, **kwargs: None) + vm = respx.post("http://browser-session.test/browser/kernel/computer/screenshot").mock( + return_value=httpx.Response(401, text="Invalid JWT") + ) + api = respx.post(f"{base_url}/browsers/sess-1/computer/screenshot").mock( + return_value=httpx.Response(200, content=b"png", headers={"content-type": "image/png"}) + ) + 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") + assert client.browser_route_cache.get("sess-1") is None + + assert vm.called + assert api.called + api_req = cast(httpx.Request, cast(Any, api.calls[0]).request) + assert api_req.headers.get("Authorization") == f"Bearer {api_key}" From 35806715e783ee1e5379d488011dcc45868097c8 Mon Sep 17 00:00:00 2001 From: tnsardesai <18272584+tnsardesai@users.noreply.github.com> Date: Fri, 21 Aug 2026 18:15:50 +0000 Subject: [PATCH 5/7] Fix ruff import order and unused lambda args Lint failed on the stale-JWT retry change. --- src/kernel/_client.py | 2 +- tests/test_browser_routing.py | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/kernel/_client.py b/src/kernel/_client.py index d41fa974..7722bf33 100644 --- a/src/kernel/_client.py +++ b/src/kernel/_client.py @@ -41,8 +41,8 @@ strip_direct_vm_auth, rewrite_direct_vm_options, browser_routing_config_from_env, - maybe_evict_browser_route_from_response, should_retry_stale_direct_vm_auth, + maybe_evict_browser_route_from_response, maybe_populate_browser_route_cache_from_response, ) diff --git a/tests/test_browser_routing.py b/tests/test_browser_routing.py index d4997d35..1c2ad765 100644 --- a/tests/test_browser_routing.py +++ b/tests/test_browser_routing.py @@ -432,9 +432,7 @@ def test_rewrite_direct_vm_options_keeps_telemetry_events_on_control_plane() -> ) cache = BrowserRouteCache() - cache.set( - BrowserRoute(session_id="sess-1", base_url="http://browser-session.test/browser/kernel", jwt="token-abc") - ) + cache.set(BrowserRoute(session_id="sess-1", base_url="http://browser-session.test/browser/kernel", jwt="token-abc")) config = BrowserRoutingConfig(subresources=("curl", "telemetry/stream", "computer", "playwright")) events = rewrite_direct_vm_options( @@ -511,9 +509,7 @@ def test_process_fs_and_telemetry_events_stay_on_api_origin_by_default( 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=[]) - ) + 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") @@ -530,7 +526,11 @@ def test_stale_direct_vm_jwt_evicts_cache_and_retries_control_plane( monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False) - monkeypatch.setattr("kernel._base_client.SyncAPIClient._sleep_for_retry", lambda *args, **kwargs: None) + + def _skip_retry_sleep(_self: object, **_kwargs: object) -> None: + return None + + monkeypatch.setattr("kernel._base_client.SyncAPIClient._sleep_for_retry", _skip_retry_sleep) vm = respx.post("http://browser-session.test/browser/kernel/computer/screenshot").mock( return_value=httpx.Response(401, text="Invalid JWT") ) From 728330d05725f765006fdc43633b4766ee6646e4 Mon Sep 17 00:00:00 2001 From: tnsardesai <18272584+tnsardesai@users.noreply.github.com> Date: Fri, 21 Aug 2026 19:02:25 +0000 Subject: [PATCH 6/7] Retry stale direct-VM auth without needing the cache A metro 401/403 with a jwt query param is enough to fall back to the control plane, so concurrent requests still retry after the first eviction. --- src/kernel/_client.py | 4 ++-- src/kernel/lib/browser_routing/routing.py | 10 +++++++--- tests/test_browser_routing.py | 13 +++++++++++++ 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/kernel/_client.py b/src/kernel/_client.py index 7722bf33..9a040e34 100644 --- a/src/kernel/_client.py +++ b/src/kernel/_client.py @@ -356,7 +356,7 @@ def _prepare_request(self, request: httpx.Request) -> None: @override def _should_retry(self, response: httpx.Response) -> bool: - if should_retry_stale_direct_vm_auth(response, cache=self.browser_route_cache): + if should_retry_stale_direct_vm_auth(response): maybe_evict_browser_route_from_response(response, cache=self.browser_route_cache) return True return super()._should_retry(response) @@ -732,7 +732,7 @@ async def _prepare_request(self, request: httpx.Request) -> None: @override def _should_retry(self, response: httpx.Response) -> bool: - if should_retry_stale_direct_vm_auth(response, cache=self.browser_route_cache): + if should_retry_stale_direct_vm_auth(response): maybe_evict_browser_route_from_response(response, cache=self.browser_route_cache) return True return super()._should_retry(response) diff --git a/src/kernel/lib/browser_routing/routing.py b/src/kernel/lib/browser_routing/routing.py index 31d8218a..1db91027 100644 --- a/src/kernel/lib/browser_routing/routing.py +++ b/src/kernel/lib/browser_routing/routing.py @@ -116,7 +116,7 @@ def maybe_evict_browser_route_from_response(response: httpx.Response, *, cache: cache.delete(session_id) return - if response.status_code not in {401, 403}: + if not is_stale_direct_vm_auth_response(response): return session_id = _session_id_from_direct_vm_response(response, cache=cache) @@ -173,10 +173,14 @@ def _session_id_from_direct_vm_response(response: httpx.Response, *, cache: Brow return None -def should_retry_stale_direct_vm_auth(response: httpx.Response, *, cache: BrowserRouteCache) -> bool: +def is_stale_direct_vm_auth_response(response: httpx.Response) -> bool: if response.status_code not in {401, 403}: return False - return _session_id_from_direct_vm_response(response, cache=cache) is not None + return bool(response.request.url.params.get("jwt")) + + +def should_retry_stale_direct_vm_auth(response: httpx.Response) -> bool: + return is_stale_direct_vm_auth_response(response) def _session_id_from_browser_delete_path(path: str) -> str | None: diff --git a/tests/test_browser_routing.py b/tests/test_browser_routing.py index 1c2ad765..c10f7b0b 100644 --- a/tests/test_browser_routing.py +++ b/tests/test_browser_routing.py @@ -546,3 +546,16 @@ def _skip_retry_sleep(_self: object, **_kwargs: object) -> None: assert api.called api_req = cast(httpx.Request, cast(Any, api.calls[0]).request) assert api_req.headers.get("Authorization") == f"Bearer {api_key}" + + +def test_stale_direct_vm_auth_retry_does_not_require_cached_route() -> None: + from kernel.lib.browser_routing.routing import should_retry_stale_direct_vm_auth + + request = httpx.Request( + "POST", + "http://browser-session.test/browser/kernel/computer/screenshot?jwt=token-abc", + ) + response = httpx.Response(401, text="Invalid JWT", request=request) + empty = BrowserRouteCache() + assert should_retry_stale_direct_vm_auth(response) is True + assert empty.get("sess-1") is None From 111aa09126aa01fc4dc88f0ed4bd27bf93d03dca Mon Sep 17 00:00:00 2001 From: tnsardesai <18272584+tnsardesai@users.noreply.github.com> Date: Fri, 21 Aug 2026 23:34:52 +0000 Subject: [PATCH 7/7] Only evict a stale JWT if it is still the cached one A later 401 must not delete a route that was refreshed in flight. --- src/kernel/lib/browser_routing/routing.py | 13 ++++++++-- tests/test_browser_routing.py | 31 +++++++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/kernel/lib/browser_routing/routing.py b/src/kernel/lib/browser_routing/routing.py index 1db91027..99bf7830 100644 --- a/src/kernel/lib/browser_routing/routing.py +++ b/src/kernel/lib/browser_routing/routing.py @@ -69,6 +69,14 @@ def set(self, route: BrowserRoute) -> None: def delete(self, session_id: str) -> None: self._routes.pop(_normalize_session_id(session_id), None) + def delete_if_jwt(self, session_id: str, jwt: str) -> bool: + key = _normalize_session_id(session_id) + route = self._routes.get(key) + if route is None or route.jwt != jwt.strip(): + return False + del self._routes[key] + return True + def values(self) -> list[BrowserRoute]: return list(self._routes.values()) @@ -119,9 +127,10 @@ def maybe_evict_browser_route_from_response(response: httpx.Response, *, cache: if not is_stale_direct_vm_auth_response(response): return + jwt = str(response.request.url.params.get("jwt") or "").strip() session_id = _session_id_from_direct_vm_response(response, cache=cache) - if session_id: - cache.delete(session_id) + if session_id and jwt: + cache.delete_if_jwt(session_id, jwt) def populate_browser_route_cache_from_value(value: object, *, cache: BrowserRouteCache) -> None: diff --git a/tests/test_browser_routing.py b/tests/test_browser_routing.py index c10f7b0b..ee73eafd 100644 --- a/tests/test_browser_routing.py +++ b/tests/test_browser_routing.py @@ -548,6 +548,37 @@ def _skip_retry_sleep(_self: object, **_kwargs: object) -> None: assert api_req.headers.get("Authorization") == f"Bearer {api_key}" +def test_stale_direct_vm_jwt_does_not_evict_refreshed_route() -> None: + from kernel.lib.browser_routing.routing import maybe_evict_browser_route_from_response + + cache = BrowserRouteCache() + cache.set( + BrowserRoute( + session_id="sess-1", + base_url="http://browser-session.test/browser/kernel", + jwt="token-abc", + ) + ) + cache.set( + BrowserRoute( + session_id="sess-1", + base_url="http://browser-session.test/browser/kernel", + jwt="jwt-FRESH", + ) + ) + request = httpx.Request( + "POST", + "http://browser-session.test/browser/kernel/computer/screenshot?jwt=token-abc", + ) + maybe_evict_browser_route_from_response( + httpx.Response(401, text="Invalid JWT", request=request), + cache=cache, + ) + route = cache.get("sess-1") + assert route is not None + assert route.jwt == "jwt-FRESH" + + def test_stale_direct_vm_auth_retry_does_not_require_cached_route() -> None: from kernel.lib.browser_routing.routing import should_retry_stale_direct_vm_auth