@@ -392,7 +392,12 @@ def test_browser_route_from_browser_requires_base_url_and_jwt() -> None:
392392
393393def 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
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
@@ -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
443471def 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