feat(chrome): let the IDE agent use the user's Chrome and beta.42
Server CI and Windows package / verify (push) Successful in 3m33s
Server CI and Windows package / windows-package (push) Successful in 7m36s

The AITURK Chrome extension now registers the user's Chrome as the desktop
backend's browser controller, the way the ChatGPT extension lets its agent
work in the user's browser. The agent's existing browser tools (navigate,
snapshot, click, type, scroll, back, press, get_images, vision) run in its
own background tabs grouped as "AITURK", or a tab the user hands over.

Backend: a local desktop lane in the browser-control broker. One controller
registered from the loopback desktop transport is offered to every desktop
session as a lane template; each session still gets its own exact scope on
first use. The lane has its own switch (browser.extension_control.local_desktop,
on by default); with no extension registered, or after it disconnects, the
normal browser backend runs unchanged. browser_vision turns the extension's
capture into the usual native/auxiliary vision result.

Bridge: forwards controller commands only to a connection that registered,
refuses unknown or oversized commands, relays results.

Extension: service-worker controller with its own native connection,
trusted chrome.debugger input (press/release only: a hover move waits 5 s on
background tabs), refusal of password and payment-card fields and of
browser, extension, file and Web Store pages, panel switch, Stop, tab
hand-over, recent actions, Alt+Shift+A and right-click "send to AITURK".
Cancelling Chrome's debugging bar stops the agent. Existing panel flows and
privacy rules are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
yilsemandClaude Opus 5.5 committed 2026-10-15 08:38:52 +03:00
1 parent 5c76c24c9c
commit 86a6621936
32 files changed
+2755 -195

No files matched your search

+38 -16
View File
@@ -45,6 +45,24 @@ from typing import Any, Callable, Dict, Optional
logger = logging.getLogger(__name__)
def lane_enabled(transport_family: Optional[str]) -> bool:
"""Feature gate for one controller lane.
The desktop IDE's own browser lane has its own switch
(``browser.extension_control.local_desktop``, on by default); every other
lane keeps the explicit ``browser.extension_control.enabled`` opt-in.
"""
from gateway.browser_control_broker import (
LOCAL_DESKTOP_TRANSPORT_FAMILY,
browser_control_enabled,
browser_control_local_enabled,
)
if transport_family == LOCAL_DESKTOP_TRANSPORT_FAMILY:
return browser_control_local_enabled()
return browser_control_enabled()
def extension_controller_available(action: str) -> bool:
"""Whether this request owns one exact controller capable of ``action``.
@@ -56,14 +74,9 @@ def extension_controller_available(action: str) -> bool:
all fail closed.
"""
try:
from gateway.browser_control_broker import (
browser_control_enabled,
get_browser_control_broker,
)
from gateway.browser_control_broker import get_browser_control_broker
from gateway.session_context import get_session_env
if not browser_control_enabled():
return False
session_id = get_session_env("HERMES_SESSION_ID", "") or None
principal_id = get_session_env("HERMES_BROWSER_CONTROL_PRINCIPAL", "") or None
transport_family = get_session_env(
@@ -71,6 +84,8 @@ def extension_controller_available(action: str) -> bool:
) or None
if not session_id or not principal_id or not transport_family:
return False
if not lane_enabled(transport_family):
return False
broker = get_browser_control_broker()
scope = broker.scope_for_session(
session_id=session_id,
@@ -99,6 +114,7 @@ def route_browser_tool(
principal_id: Optional[str] = None,
transport_family: Optional[str] = None,
tool_call_id: Optional[str] = "",
transform: Optional[Callable[[Any], Any]] = None,
) -> Any:
"""Route one browser action through the extension-control broker.
@@ -127,6 +143,11 @@ def route_browser_tool(
Hermes callers.
tool_call_id:
Caller tool-call id forwarded verbatim to ``dispatch``.
transform:
Optional post-processor for a *controller* result only (never for the
fallback). Its return value is the tool result as-is, so it may build
the multimodal envelope (``browser_vision`` turns the controller's
screenshot into the same result the legacy backend returns).
Returns
-------
@@ -184,6 +205,8 @@ def route_browser_tool(
result = broker.dispatch(
scope, action=action, arguments=args, tool_call_id=tool_call_id
)
if transform is not None:
return transform(result)
if isinstance(result, str):
return result
return json.dumps(result, ensure_ascii=False)
@@ -215,6 +238,7 @@ def routed_browser_handler(
principal_id: Optional[str] = None,
transport_family: Optional[str] = None,
tool_call_id: Optional[str] = None,
transform: Optional[Callable[[Any], Any]] = None,
) -> Any:
"""Lazy registry-handler route wrapper for ``browser_*`` tools.
@@ -225,10 +249,7 @@ def routed_browser_handler(
legacy handler runs unchanged.
"""
try:
from gateway.browser_control_broker import (
browser_control_enabled,
get_browser_control_broker,
)
from gateway.browser_control_broker import get_browser_control_broker
except Exception as exc: # pragma: no cover - defensive, gateway always present
logger.debug(
"browser extension router unavailable (%s); using legacy backend",
@@ -236,12 +257,6 @@ def routed_browser_handler(
)
return fallback()
if not browser_control_enabled():
return fallback()
if tool_call_id is None:
tool_call_id = current_tool_call_id()
try:
from gateway.session_context import get_session_env
@@ -255,6 +270,12 @@ def routed_browser_handler(
except Exception:
pass
if not lane_enabled(transport_family):
return fallback()
if tool_call_id is None:
tool_call_id = current_tool_call_id()
return route_browser_tool(
action,
args,
@@ -266,4 +287,5 @@ def routed_browser_handler(
principal_id=principal_id,
transport_family=transport_family,
tool_call_id=tool_call_id,
transform=transform,
)