Skip to content

Commit 5d878a2

Browse files
committed
Route process calls directly to browser VMs
1 parent a19ca06 commit 5d878a2

2 files changed

Lines changed: 18 additions & 12 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", "computer", "playwright"))
47+
return BrowserRoutingConfig(subresources=("curl", "telemetry/stream", "computer", "playwright", "process"))
4848
if raw.strip() == "":
4949
return BrowserRoutingConfig()
5050

tests/test_browser_routing.py

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,7 @@ def test_browser_routing_config_from_env_defaults(monkeypatch: pytest.MonkeyPatc
397397
"telemetry/stream",
398398
"computer",
399399
"playwright",
400+
"process",
400401
)
401402

402403

@@ -406,7 +407,7 @@ def test_direct_vm_routing_allowlist_segment_boundary() -> None:
406407
# stream-prefixed-but-different path is not matched.
407408
from kernel.lib.browser_routing.routing import _matches_direct_vm_prefix
408409

409-
prefixes = ("curl", "telemetry/stream", "computer", "playwright")
410+
prefixes = ("curl", "telemetry/stream", "computer", "playwright", "process")
410411
assert _matches_direct_vm_prefix("telemetry/stream", prefixes) is True
411412
assert _matches_direct_vm_prefix("telemetry/stream/x", prefixes) is True
412413
assert _matches_direct_vm_prefix("telemetry/events", prefixes) is False
@@ -415,7 +416,8 @@ def test_direct_vm_routing_allowlist_segment_boundary() -> None:
415416
assert _matches_direct_vm_prefix("curl/raw", prefixes) is True
416417
assert _matches_direct_vm_prefix("computer/screenshot", prefixes) is True
417418
assert _matches_direct_vm_prefix("playwright/execute", prefixes) is True
418-
assert _matches_direct_vm_prefix("process/exec", prefixes) is False
419+
assert _matches_direct_vm_prefix("process/exec", prefixes) is True
420+
assert _matches_direct_vm_prefix("process/proc-1/stdout/stream", prefixes) is True
419421
assert _matches_direct_vm_prefix("fs/read", prefixes) is False
420422

421423

@@ -433,7 +435,7 @@ def test_rewrite_direct_vm_options_keeps_telemetry_events_on_control_plane() ->
433435

434436
cache = BrowserRouteCache()
435437
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"))
438+
config = BrowserRoutingConfig(subresources=("curl", "telemetry/stream", "computer", "playwright", "process"))
437439

438440
events = rewrite_direct_vm_options(
439441
FinalRequestOptions(method="get", url="/browsers/sess-1/telemetry/events"), cache=cache, config=config
@@ -458,7 +460,7 @@ def test_rewrite_direct_vm_options_keeps_telemetry_events_on_control_plane() ->
458460
process = rewrite_direct_vm_options(
459461
FinalRequestOptions(method="post", url="/browsers/sess-1/process/exec"), cache=cache, config=config
460462
)
461-
assert process.url == "/browsers/sess-1/process/exec"
463+
assert str(process.url).startswith("http://browser-session.test/browser/kernel/process/exec")
462464

463465
fs_read = rewrite_direct_vm_options(
464466
FinalRequestOptions(method="get", url="/browsers/sess-1/fs/read_file"), cache=cache, config=config
@@ -472,7 +474,7 @@ def test_browser_routing_config_from_env_empty_string_disables_routing(monkeypat
472474

473475

474476
@respx.mock
475-
def test_computer_screenshot_and_playwright_execute_route_to_vm_by_default(
477+
def test_default_browser_subresources_route_to_vm(
476478
monkeypatch: pytest.MonkeyPatch,
477479
) -> None:
478480
monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
@@ -482,10 +484,14 @@ def test_computer_screenshot_and_playwright_execute_route_to_vm_by_default(
482484
execute = respx.post("http://browser-session.test/browser/kernel/playwright/execute").mock(
483485
return_value=httpx.Response(200, json={"success": True})
484486
)
487+
process = respx.post("http://browser-session.test/browser/kernel/process/exec").mock(
488+
return_value=httpx.Response(200, json={"exit_code": 0, "stdout_b64": "", "stderr_b64": ""})
489+
)
485490
with Kernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) as client:
486491
_cache_browser(client)
487492
client.browsers.computer.capture_screenshot("sess-1")
488493
out = client.browsers.playwright.execute("sess-1", code="return 1")
494+
process_out = client.browsers.process.exec("sess-1", command="echo")
489495

490496
assert screenshot.called
491497
screenshot_req = cast(httpx.Request, cast(Any, screenshot.calls[0]).request)
@@ -495,28 +501,28 @@ def test_computer_screenshot_and_playwright_execute_route_to_vm_by_default(
495501
execute_req = cast(httpx.Request, cast(Any, execute.calls[0]).request)
496502
assert execute_req.url.params.get("jwt") == "token-abc"
497503
assert execute_req.headers.get("Authorization") is None
504+
assert process.called
505+
process_req = cast(httpx.Request, cast(Any, process.calls[0]).request)
506+
assert process_req.url.params.get("jwt") == "token-abc"
507+
assert process_req.headers.get("Authorization") is None
498508
assert out.success is True
509+
assert process_out.exit_code == 0
499510

500511

501512
@respx.mock
502-
def test_process_fs_and_telemetry_events_stay_on_api_origin_by_default(
513+
def test_fs_and_telemetry_events_stay_on_api_origin_by_default(
503514
monkeypatch: pytest.MonkeyPatch,
504515
) -> None:
505516
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-
)
509517
fs_read = respx.get(f"{base_url}/browsers/sess-1/fs/read_file").mock(
510518
return_value=httpx.Response(200, content=b"x", headers={"content-type": "application/octet-stream"})
511519
)
512520
events = respx.get(f"{base_url}/browsers/sess-1/telemetry/events").mock(return_value=httpx.Response(200, json=[]))
513521
with Kernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) as client:
514522
_cache_browser(client)
515-
client.browsers.process.exec("sess-1", command="echo")
516523
client.browsers.fs.read_file("sess-1", path="/tmp/x")
517524
client.browsers.telemetry.events("sess-1")
518525

519-
assert process.called
520526
assert fs_read.called
521527
assert events.called
522528

0 commit comments

Comments
 (0)