Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
"""Registry-level browser extension router.
|
||||
|
||||
This module is the *agent-side* half of the browser-extension-control
|
||||
feature: it decides, for one registry ``browser_*`` handler invocation,
|
||||
whether the command is executed by an attached extension controller (via
|
||||
the :mod:`gateway.browser_control_broker`) or by the existing legacy
|
||||
browser backend.
|
||||
|
||||
Routing contract (exercised by ``tests/tools/test_browser_extension_router.py``):
|
||||
|
||||
- **Feature off ⇒ legacy, untouched.** When ``enabled`` is false the broker
|
||||
is never touched and ``fallback()`` is called exactly once. This is the
|
||||
default: ``browser.extension_control.enabled`` is false unless explicitly
|
||||
configured, so every real browser action keeps its exact legacy path.
|
||||
|
||||
- **No server-bound identity ⇒ legacy.** Generic Hermes callers keep the
|
||||
existing backend when no authenticated browser-controller identity is bound.
|
||||
|
||||
- **Bound identity ⇒ authoritative extension lane.** Once the gateway binds a
|
||||
browser-controller principal and transport family, missing/ambiguous scope,
|
||||
disconnect, or capability mismatch fail closed. A "control this tab" turn
|
||||
must never jump to an unrelated local/cloud browser backend.
|
||||
|
||||
- **Selected controller ⇒ authoritative.** Once a controller is selected the
|
||||
command is dispatched to it and its result returned; the legacy backend
|
||||
is *never* retried, even when the controller fails (timeout, cancellation,
|
||||
rejection, transport error all propagate to the caller).
|
||||
|
||||
- **Arguments are never mutated.** ``args`` is passed through untouched;
|
||||
the broker copies arguments into its command frame itself.
|
||||
|
||||
The lazy wrapper :func:`routed_browser_handler` is what the ``browser_*``
|
||||
registry handlers call. It resolves the feature flag and the process-local
|
||||
broker lazily on every invocation so importing this module (or
|
||||
``tools.browser_tool``) never pulls in the gateway, and so a mid-process
|
||||
config change is honored without restart.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Any, Callable, Dict, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def extension_controller_available(action: str) -> bool:
|
||||
"""Whether this request owns one exact controller capable of ``action``.
|
||||
|
||||
Tool-schema assembly runs inside the API request's session context, before
|
||||
a model can call a browser tool. The legacy browser backend's availability
|
||||
probe cannot decide whether the extension route is usable, so routeable
|
||||
tools consult the process-local broker directly. Missing server-bound
|
||||
identity, ambiguous scope, a detached controller, or a capability mismatch
|
||||
all fail closed.
|
||||
"""
|
||||
try:
|
||||
from gateway.browser_control_broker import (
|
||||
browser_control_enabled,
|
||||
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(
|
||||
"HERMES_BROWSER_CONTROL_TRANSPORT_FAMILY", ""
|
||||
) or None
|
||||
if not session_id or not principal_id or not transport_family:
|
||||
return False
|
||||
broker = get_browser_control_broker()
|
||||
scope = broker.scope_for_session(
|
||||
session_id=session_id,
|
||||
principal_id=principal_id,
|
||||
transport_family=transport_family,
|
||||
)
|
||||
return scope is not None and broker.select(scope, action) is not None
|
||||
except Exception:
|
||||
logger.debug(
|
||||
"browser extension availability check failed for %s",
|
||||
action,
|
||||
exc_info=True,
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def route_browser_tool(
|
||||
action: str,
|
||||
args: Dict[str, Any],
|
||||
*,
|
||||
fallback: Callable[[], Any],
|
||||
broker: Any,
|
||||
enabled: bool,
|
||||
session_id: Optional[str] = None,
|
||||
task_id: Optional[str] = None,
|
||||
principal_id: Optional[str] = None,
|
||||
transport_family: Optional[str] = None,
|
||||
tool_call_id: Optional[str] = "",
|
||||
) -> Any:
|
||||
"""Route one browser action through the extension-control broker.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
action:
|
||||
Registry tool name / controller capability, e.g. ``"browser_navigate"``.
|
||||
args:
|
||||
Tool arguments as received from the model. Never mutated.
|
||||
fallback:
|
||||
The existing backend handler, called exactly once when the feature is
|
||||
off or no server-bound controller identity exists. Must be a
|
||||
zero-argument callable.
|
||||
broker:
|
||||
Object exposing ``scope_for_session(**identity) -> scope|None``,
|
||||
``select(scope, capability) -> controller|None`` and
|
||||
``dispatch(scope, *, action, arguments, tool_call_id)``. The real
|
||||
implementation is ``gateway.browser_control_broker``.
|
||||
enabled:
|
||||
Feature flag; false bypasses the broker entirely.
|
||||
session_id/task_id:
|
||||
Caller session hints forwarded to ``scope_for_session``.
|
||||
principal_id/transport_family:
|
||||
Server-bound caller identity. Both are mandatory when the feature is
|
||||
enabled; missing values preserve the existing backend for generic
|
||||
Hermes callers.
|
||||
tool_call_id:
|
||||
Caller tool-call id forwarded verbatim to ``dispatch``.
|
||||
|
||||
Returns
|
||||
-------
|
||||
The legacy backend's return value when falling back, or the controller's
|
||||
completion result when routed. Exceptions from a selected controller are
|
||||
propagated — the legacy backend is never retried after selection.
|
||||
"""
|
||||
if not enabled:
|
||||
return fallback()
|
||||
|
||||
if not str(principal_id or "").strip() or not str(transport_family or "").strip():
|
||||
return fallback()
|
||||
|
||||
scope = broker.scope_for_session(
|
||||
session_id=session_id,
|
||||
task_id=task_id,
|
||||
principal_id=principal_id,
|
||||
transport_family=transport_family,
|
||||
)
|
||||
if scope is None:
|
||||
# A stamped identity alone does not make the extension lane
|
||||
# authoritative — authentication happens at transport auth, but the
|
||||
# lane only BINDS when a controller actually registers for it. If no
|
||||
# controller ever registered, generic callers keep the legacy
|
||||
# backend. Once a lane registered (even if the controller is
|
||||
# currently offline/ambiguous), fail closed: a "control this tab"
|
||||
# session must never silently jump to an unrelated browser.
|
||||
lane_bound = getattr(broker, "lane_registered", None)
|
||||
if callable(lane_bound) and not lane_bound(
|
||||
session_id=session_id,
|
||||
task_id=task_id,
|
||||
principal_id=principal_id,
|
||||
transport_family=transport_family,
|
||||
):
|
||||
return fallback()
|
||||
from gateway.browser_control_broker import ControllerUnavailable
|
||||
|
||||
raise ControllerUnavailable(
|
||||
f"bound browser controller unavailable for {action}"
|
||||
)
|
||||
|
||||
controller = broker.select(scope, action)
|
||||
if controller is None:
|
||||
from gateway.browser_control_broker import ControllerUnavailable
|
||||
|
||||
raise ControllerUnavailable(
|
||||
f"bound browser controller cannot execute {action}"
|
||||
)
|
||||
|
||||
# A controller was selected: it is authoritative. Never retry through the
|
||||
# existing backend, whatever happens here. Registry handlers must return a
|
||||
# string (or the dedicated multimodal envelope), while controller transports
|
||||
# naturally complete with decoded JSON values. Preserve existing string
|
||||
# results byte-for-byte and serialize decoded values at this boundary.
|
||||
result = broker.dispatch(
|
||||
scope, action=action, arguments=args, tool_call_id=tool_call_id
|
||||
)
|
||||
if isinstance(result, str):
|
||||
return result
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
|
||||
|
||||
def current_tool_call_id() -> str:
|
||||
"""Return the active tool_call_id, or ``""`` when none is bound.
|
||||
|
||||
The agent executor binds the id via
|
||||
``tools.approval.set_current_observability_context`` immediately before
|
||||
registry dispatch, so the registry handler (and this router) can read it
|
||||
back from the same context. Bare/offline callers have no binding.
|
||||
"""
|
||||
try:
|
||||
from tools.approval import _approval_tool_call_id
|
||||
|
||||
return _approval_tool_call_id.get() or ""
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def routed_browser_handler(
|
||||
action: str,
|
||||
args: Dict[str, Any],
|
||||
*,
|
||||
fallback: Callable[[], Any],
|
||||
task_id: Optional[str] = None,
|
||||
session_id: Optional[str] = None,
|
||||
principal_id: Optional[str] = None,
|
||||
transport_family: Optional[str] = None,
|
||||
tool_call_id: Optional[str] = None,
|
||||
) -> Any:
|
||||
"""Lazy registry-handler route wrapper for ``browser_*`` tools.
|
||||
|
||||
Resolves the feature flag and process-local broker lazily so the
|
||||
default (feature off) path costs one cached config read and an immediate
|
||||
fallback, and so importing ``tools.browser_tool`` never imports the
|
||||
gateway. When the gateway cannot be imported or the feature is off, the
|
||||
legacy handler runs unchanged.
|
||||
"""
|
||||
try:
|
||||
from gateway.browser_control_broker import (
|
||||
browser_control_enabled,
|
||||
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",
|
||||
exc,
|
||||
)
|
||||
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
|
||||
|
||||
session_id = session_id or get_session_env("HERMES_SESSION_ID", "") or None
|
||||
principal_id = principal_id or get_session_env(
|
||||
"HERMES_BROWSER_CONTROL_PRINCIPAL", ""
|
||||
) or None
|
||||
transport_family = transport_family or get_session_env(
|
||||
"HERMES_BROWSER_CONTROL_TRANSPORT_FAMILY", ""
|
||||
) or None
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return route_browser_tool(
|
||||
action,
|
||||
args,
|
||||
fallback=fallback,
|
||||
broker=get_browser_control_broker(),
|
||||
enabled=True,
|
||||
session_id=session_id,
|
||||
task_id=task_id,
|
||||
principal_id=principal_id,
|
||||
transport_family=transport_family,
|
||||
tool_call_id=tool_call_id,
|
||||
)
|
||||
Reference in New Issue
Block a user