@@ -390,9 +390,14 @@ def test_browser_route_from_browser_requires_base_url_and_jwt() -> None:
390390 assert browser_route_from_browser ({** _fake_browser (), "cdp_ws_url" : None }) is None
391391
392392
393- def test_browser_routing_config_from_env_defaults_to_curl (monkeypatch : pytest .MonkeyPatch ) -> None :
393+ def test_browser_routing_config_from_env_defaults (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
398403def 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
@@ -424,10 +432,8 @@ def test_rewrite_direct_vm_options_keeps_telemetry_events_on_control_plane() ->
424432 )
425433
426434 cache = BrowserRouteCache ()
427- cache .set (
428- BrowserRoute (session_id = "sess-1" , base_url = "http://browser-session.test/browser/kernel" , jwt = "token-abc" )
429- )
430- config = BrowserRoutingConfig (subresources = ("curl" , "telemetry/stream" ))
435+ cache .set (BrowserRoute (session_id = "sess-1" , base_url = "http://browser-session.test/browser/kernel" , jwt = "token-abc" ))
436+ config = BrowserRoutingConfig (subresources = ("curl" , "telemetry/stream" , "computer" , "playwright" ))
431437
432438 events = rewrite_direct_vm_options (
433439 FinalRequestOptions (method = "get" , url = "/browsers/sess-1/telemetry/events" ), cache = cache , config = config
@@ -439,7 +445,148 @@ def test_rewrite_direct_vm_options_keeps_telemetry_events_on_control_plane() ->
439445 )
440446 assert str (stream .url ).startswith ("http://browser-session.test/browser/kernel/telemetry/stream" )
441447
448+ screenshot = rewrite_direct_vm_options (
449+ FinalRequestOptions (method = "post" , url = "/browsers/sess-1/computer/screenshot" ), cache = cache , config = config
450+ )
451+ assert str (screenshot .url ).startswith ("http://browser-session.test/browser/kernel/computer/screenshot" )
452+
453+ execute = rewrite_direct_vm_options (
454+ FinalRequestOptions (method = "post" , url = "/browsers/sess-1/playwright/execute" ), cache = cache , config = config
455+ )
456+ assert str (execute .url ).startswith ("http://browser-session.test/browser/kernel/playwright/execute" )
457+
458+ process = rewrite_direct_vm_options (
459+ FinalRequestOptions (method = "post" , url = "/browsers/sess-1/process/exec" ), cache = cache , config = config
460+ )
461+ assert process .url == "/browsers/sess-1/process/exec"
462+
463+ fs_read = rewrite_direct_vm_options (
464+ FinalRequestOptions (method = "get" , url = "/browsers/sess-1/fs/read_file" ), cache = cache , config = config
465+ )
466+ assert fs_read .url == "/browsers/sess-1/fs/read_file"
467+
442468
443469def test_browser_routing_config_from_env_empty_string_disables_routing (monkeypatch : pytest .MonkeyPatch ) -> None :
444470 monkeypatch .setenv ("KERNEL_BROWSER_ROUTING_SUBRESOURCES" , "" )
445471 assert browser_routing_config_from_env ().subresources == ()
472+
473+
474+ @respx .mock
475+ def test_computer_screenshot_and_playwright_execute_route_to_vm_by_default (
476+ monkeypatch : pytest .MonkeyPatch ,
477+ ) -> None :
478+ monkeypatch .delenv ("KERNEL_BROWSER_ROUTING_SUBRESOURCES" , raising = False )
479+ screenshot = respx .post ("http://browser-session.test/browser/kernel/computer/screenshot" ).mock (
480+ return_value = httpx .Response (200 , content = b"png" , headers = {"content-type" : "image/png" })
481+ )
482+ execute = respx .post ("http://browser-session.test/browser/kernel/playwright/execute" ).mock (
483+ return_value = httpx .Response (200 , json = {"success" : True })
484+ )
485+ with Kernel (base_url = base_url , api_key = api_key , _strict_response_validation = True ) as client :
486+ _cache_browser (client )
487+ client .browsers .computer .capture_screenshot ("sess-1" )
488+ out = client .browsers .playwright .execute ("sess-1" , code = "return 1" )
489+
490+ assert screenshot .called
491+ screenshot_req = cast (httpx .Request , cast (Any , screenshot .calls [0 ]).request )
492+ assert screenshot_req .url .params .get ("jwt" ) == "token-abc"
493+ assert screenshot_req .headers .get ("Authorization" ) is None
494+ assert execute .called
495+ execute_req = cast (httpx .Request , cast (Any , execute .calls [0 ]).request )
496+ assert execute_req .url .params .get ("jwt" ) == "token-abc"
497+ assert execute_req .headers .get ("Authorization" ) is None
498+ assert out .success is True
499+
500+
501+ @respx .mock
502+ def test_process_fs_and_telemetry_events_stay_on_api_origin_by_default (
503+ monkeypatch : pytest .MonkeyPatch ,
504+ ) -> None :
505+ monkeypatch .delenv ("KERNEL_BROWSER_ROUTING_SUBRESOURCES" , raising = False )
506+ process = respx .post (f"{ base_url } /browsers/sess-1/process/exec" ).mock (
507+ return_value = httpx .Response (200 , json = {"exit_code" : 0 , "stdout_b64" : "" , "stderr_b64" : "" })
508+ )
509+ fs_read = respx .get (f"{ base_url } /browsers/sess-1/fs/read_file" ).mock (
510+ return_value = httpx .Response (200 , content = b"x" , headers = {"content-type" : "application/octet-stream" })
511+ )
512+ events = respx .get (f"{ base_url } /browsers/sess-1/telemetry/events" ).mock (return_value = httpx .Response (200 , json = []))
513+ with Kernel (base_url = base_url , api_key = api_key , _strict_response_validation = True ) as client :
514+ _cache_browser (client )
515+ client .browsers .process .exec ("sess-1" , command = "echo" )
516+ client .browsers .fs .read_file ("sess-1" , path = "/tmp/x" )
517+ client .browsers .telemetry .events ("sess-1" )
518+
519+ assert process .called
520+ assert fs_read .called
521+ assert events .called
522+
523+
524+ @respx .mock
525+ def test_stale_direct_vm_jwt_evicts_cache_and_retries_control_plane (
526+ monkeypatch : pytest .MonkeyPatch ,
527+ ) -> None :
528+ monkeypatch .delenv ("KERNEL_BROWSER_ROUTING_SUBRESOURCES" , raising = False )
529+
530+ def _skip_retry_sleep (_self : object , ** _kwargs : object ) -> None :
531+ return None
532+
533+ monkeypatch .setattr ("kernel._base_client.SyncAPIClient._sleep_for_retry" , _skip_retry_sleep )
534+ vm = respx .post ("http://browser-session.test/browser/kernel/computer/screenshot" ).mock (
535+ return_value = httpx .Response (401 , text = "Invalid JWT" )
536+ )
537+ api = respx .post (f"{ base_url } /browsers/sess-1/computer/screenshot" ).mock (
538+ return_value = httpx .Response (200 , content = b"png" , headers = {"content-type" : "image/png" })
539+ )
540+ with Kernel (base_url = base_url , api_key = api_key , _strict_response_validation = True ) as client :
541+ _cache_browser (client )
542+ client .browsers .computer .capture_screenshot ("sess-1" )
543+ assert client .browser_route_cache .get ("sess-1" ) is None
544+
545+ assert vm .called
546+ assert api .called
547+ api_req = cast (httpx .Request , cast (Any , api .calls [0 ]).request )
548+ assert api_req .headers .get ("Authorization" ) == f"Bearer { api_key } "
549+
550+
551+ def test_stale_direct_vm_jwt_does_not_evict_refreshed_route () -> None :
552+ from kernel .lib .browser_routing .routing import maybe_evict_browser_route_from_response
553+
554+ cache = BrowserRouteCache ()
555+ cache .set (
556+ BrowserRoute (
557+ session_id = "sess-1" ,
558+ base_url = "http://browser-session.test/browser/kernel" ,
559+ jwt = "token-abc" ,
560+ )
561+ )
562+ cache .set (
563+ BrowserRoute (
564+ session_id = "sess-1" ,
565+ base_url = "http://browser-session.test/browser/kernel" ,
566+ jwt = "jwt-FRESH" ,
567+ )
568+ )
569+ request = httpx .Request (
570+ "POST" ,
571+ "http://browser-session.test/browser/kernel/computer/screenshot?jwt=token-abc" ,
572+ )
573+ maybe_evict_browser_route_from_response (
574+ httpx .Response (401 , text = "Invalid JWT" , request = request ),
575+ cache = cache ,
576+ )
577+ route = cache .get ("sess-1" )
578+ assert route is not None
579+ assert route .jwt == "jwt-FRESH"
580+
581+
582+ def test_stale_direct_vm_auth_retry_does_not_require_cached_route () -> None :
583+ from kernel .lib .browser_routing .routing import should_retry_stale_direct_vm_auth
584+
585+ request = httpx .Request (
586+ "POST" ,
587+ "http://browser-session.test/browser/kernel/computer/screenshot?jwt=token-abc" ,
588+ )
589+ response = httpx .Response (401 , text = "Invalid JWT" , request = request )
590+ empty = BrowserRouteCache ()
591+ assert should_retry_stale_direct_vm_auth (response ) is True
592+ assert empty .get ("sess-1" ) is None
0 commit comments