feat(chrome): let the IDE agent use the user's Chrome and beta.42
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:
1 parent
5c76c24c9c
commit
86a6621936
32 files changed
+2755
-195
No files matched your search
@@ -38,6 +38,8 @@ import logging
|
||||
|
||||
from gateway.browser_control_broker import (
|
||||
BROWSER_CONTROL_PROTOCOL_VERSION,
|
||||
LOCAL_DESKTOP_PRINCIPAL,
|
||||
LOCAL_DESKTOP_TRANSPORT_FAMILY,
|
||||
browser_control_protocol_supported,
|
||||
filter_browser_control_capabilities,
|
||||
)
|
||||
@@ -58,9 +60,17 @@ method = _registry.method
|
||||
#: transport can never address a dashboard controller (and vice versa).
|
||||
_CLOUD_TRANSPORT_FAMILY = "cloud-ticket-ws"
|
||||
|
||||
#: Local desktop lane: the AITURK Chrome extension reaches this backend only
|
||||
#: through the desktop IDE's native bridge, which authenticates with the
|
||||
#: loopback session token (``WSTransport.local_desktop``).
|
||||
_LOCAL_TRANSPORT_FAMILY = LOCAL_DESKTOP_TRANSPORT_FAMILY
|
||||
_LOCAL_PRINCIPAL = LOCAL_DESKTOP_PRINCIPAL
|
||||
|
||||
#: JSON-RPC error code for identity / session / flag denials (forbidden).
|
||||
_ERR_FORBIDDEN = 4403
|
||||
|
||||
_ID_LIMIT = 200
|
||||
|
||||
|
||||
def _is_authenticated_identity(identity: object) -> bool:
|
||||
"""True for a server-minted, non-internal ``{user_id, provider}`` identity."""
|
||||
@@ -377,6 +387,130 @@ def _(
|
||||
return _ok(rid, {"detached": True})
|
||||
|
||||
|
||||
def _is_local_desktop(transport: object) -> bool:
|
||||
return getattr(transport, "local_desktop", False) is True
|
||||
|
||||
|
||||
@method("browser.controller.local.register")
|
||||
def _(
|
||||
rid,
|
||||
params: dict,
|
||||
_family=_LOCAL_TRANSPORT_FAMILY,
|
||||
_principal=_LOCAL_PRINCIPAL,
|
||||
_protocol_supported=browser_control_protocol_supported,
|
||||
_protocol_version=BROWSER_CONTROL_PROTOCOL_VERSION,
|
||||
_filter_capabilities=filter_browser_control_capabilities,
|
||||
_forbidden=_ERR_FORBIDDEN,
|
||||
_local=_is_local_desktop,
|
||||
_event_writer=_broker_event_writer,
|
||||
_id_limit=_ID_LIMIT,
|
||||
) -> dict:
|
||||
"""Offer the desktop user's own browser to every desktop session.
|
||||
|
||||
Only the loopback desktop transport (the IDE's native Chrome bridge) may
|
||||
register, and only while ``browser.extension_control.local_desktop`` is
|
||||
on. The controller becomes a lane template: each desktop session that
|
||||
uses a browser tool gets its own exact scope on first use, and its frames
|
||||
carry that session id. Registering again from the same transport
|
||||
refreshes it; closing the transport removes it, so sessions go back to
|
||||
their normal browser backend.
|
||||
"""
|
||||
from gateway import browser_control_broker
|
||||
|
||||
transport = current_transport()
|
||||
if not _local(transport):
|
||||
return _err(
|
||||
rid, _forbidden, "local browser controller requires the desktop transport"
|
||||
)
|
||||
if not browser_control_broker.browser_control_local_enabled():
|
||||
return _err(rid, _forbidden, "browser.extension_control.local_desktop is off")
|
||||
if not _protocol_supported(params.get("protocol_version")):
|
||||
return _err(
|
||||
rid,
|
||||
_forbidden,
|
||||
f"unsupported browser-control protocol version; expected {_protocol_version}",
|
||||
)
|
||||
controller_id = str(params.get("controller_id") or "").strip()
|
||||
browser_profile_id = str(params.get("browser_profile_id") or "").strip()
|
||||
profile_id = str(params.get("profile") or "local").strip() or "local"
|
||||
if (
|
||||
not controller_id
|
||||
or not browser_profile_id
|
||||
or max(len(controller_id), len(browser_profile_id), len(profile_id)) > _id_limit
|
||||
):
|
||||
return _err(rid, _forbidden, "controller_id and browser_profile_id are required")
|
||||
capabilities = _filter_capabilities(params.get("capabilities"))
|
||||
if not capabilities:
|
||||
return _err(rid, _forbidden, "no permitted controller capabilities requested")
|
||||
template = browser_control_broker.ControllerScope(
|
||||
principal_id=_principal,
|
||||
profile_id=profile_id,
|
||||
session_id=None,
|
||||
controller_id=controller_id,
|
||||
browser_profile_id=browser_profile_id,
|
||||
transport_family=_family,
|
||||
capabilities=capabilities,
|
||||
)
|
||||
broker = browser_control_broker.get_browser_control_broker()
|
||||
broker.attach_lane_template(
|
||||
template,
|
||||
lambda session_id: _event_writer(transport, session_id),
|
||||
owner=transport,
|
||||
)
|
||||
return _ok(
|
||||
rid,
|
||||
{
|
||||
"registered": True,
|
||||
"transport_family": _family,
|
||||
"capabilities": sorted(capabilities),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@method("browser.controller.local.result")
|
||||
def _(rid, params: dict, _forbidden=_ERR_FORBIDDEN, _local=_is_local_desktop) -> dict:
|
||||
"""Deliver one local controller result; only its own transport may."""
|
||||
from gateway import browser_control_broker
|
||||
|
||||
transport = current_transport()
|
||||
if not _local(transport):
|
||||
return _err(rid, _forbidden, "local browser controller requires the desktop transport")
|
||||
command_id = str(params.get("command_id") or "")
|
||||
if not command_id:
|
||||
return _err(rid, _forbidden, "command_id required")
|
||||
ok = params.get("ok") is True
|
||||
accepted = browser_control_broker.get_browser_control_broker().complete_for_owner(
|
||||
command_id,
|
||||
owner=transport,
|
||||
ok=ok,
|
||||
result=params.get("result") if ok else params.get("error"),
|
||||
)
|
||||
return _ok(rid, {"accepted": accepted})
|
||||
|
||||
|
||||
@method("browser.controller.local.detach")
|
||||
def _(
|
||||
rid,
|
||||
params: dict,
|
||||
_family=_LOCAL_TRANSPORT_FAMILY,
|
||||
_principal=_LOCAL_PRINCIPAL,
|
||||
_forbidden=_ERR_FORBIDDEN,
|
||||
_local=_is_local_desktop,
|
||||
) -> dict:
|
||||
"""Stop offering the user's browser; pending commands are cancelled."""
|
||||
from gateway import browser_control_broker
|
||||
|
||||
transport = current_transport()
|
||||
if not _local(transport):
|
||||
return _err(rid, _forbidden, "local browser controller requires the desktop transport")
|
||||
detached = browser_control_broker.get_browser_control_broker().detach_lane_template(
|
||||
principal_id=_principal,
|
||||
transport_family=_family,
|
||||
owner=transport,
|
||||
)
|
||||
return _ok(rid, {"detached": detached})
|
||||
|
||||
|
||||
def register(server) -> None:
|
||||
"""Bind this module's handlers onto ``server``'s globals and registry."""
|
||||
_registry.install(server)
|
||||
Reference in new issue
Block a user