-
Notifications
You must be signed in to change notification settings - Fork 0
Local control: in-SDK sidecar + desktop/browser drivers #161
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
ea3a77a
3f6c600
f6d3585
0c176a2
7e00149
a26e295
890d7db
e343d15
11f4bd1
0d1c561
8a1810f
80a969d
0460ef2
fded9ce
7345734
32ce8a5
aeca68a
db90a85
b3006fd
2a055f1
1c267dc
d25d885
abf44a5
73d1fc9
576e89a
4e8401e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from .chrome import ensure_local_chrome | ||
| from .config import SidecarConfig, session_id_from_environment_id | ||
| from .runtime import ensure_sidecar, stop_sidecars | ||
| from .sidecar import SidecarBusyError, SidecarClient | ||
|
|
||
| __all__ = [ | ||
| "SidecarBusyError", | ||
| "SidecarClient", | ||
| "SidecarConfig", | ||
| "ensure_local_chrome", | ||
| "ensure_sidecar", | ||
| "session_id_from_environment_id", | ||
| "stop_sidecars", | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import logging | ||
| import platform | ||
| import shutil | ||
| import subprocess | ||
| import time | ||
| from pathlib import Path | ||
|
|
||
| import httpx | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| DEFAULT_DEBUG_PORT = 9222 | ||
| CHROME_STARTUP_TIMEOUT_S = 20.0 | ||
| CHROME_PROFILE_DIR = Path.home() / ".hai" / "chrome-profile" | ||
|
|
||
| _CHROME_CANDIDATES = { | ||
| "Darwin": ( | ||
| "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", | ||
| "/Applications/Chromium.app/Contents/MacOS/Chromium", | ||
| ), | ||
| "Windows": ( | ||
| r"C:\Program Files\Google\Chrome\Application\chrome.exe", | ||
| r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe", | ||
| ), | ||
| } | ||
| _CHROME_COMMANDS = ("google-chrome", "google-chrome-stable", "chromium", "chromium-browser", "chrome") | ||
|
|
||
|
|
||
| def ensure_local_chrome(port: int = DEFAULT_DEBUG_PORT) -> None: | ||
| """Make sure a Chrome with an open remote-debugging port is running, launching one if needed.""" | ||
| if _debugger_listening(port): | ||
| return | ||
| binary = _find_chrome() | ||
| if binary is None: | ||
| raise RuntimeError( | ||
| "Google Chrome was not found. Install Chrome, or start a browser yourself with " | ||
| f"--remote-debugging-port={port}." | ||
| ) | ||
| CHROME_PROFILE_DIR.mkdir(parents=True, exist_ok=True) | ||
| logger.info("launching Chrome with remote debugging on port %d (profile: %s)", port, CHROME_PROFILE_DIR) | ||
| subprocess.Popen( | ||
| [ | ||
| binary, | ||
| f"--remote-debugging-port={port}", | ||
| f"--user-data-dir={CHROME_PROFILE_DIR}", | ||
| "--no-first-run", | ||
| "--no-default-browser-check", | ||
| ], | ||
| stdin=subprocess.DEVNULL, | ||
| stdout=subprocess.DEVNULL, | ||
| stderr=subprocess.DEVNULL, | ||
| start_new_session=True, | ||
| ) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Duplicate Chrome launch raceMedium Severity
Reviewed by Cursor Bugbot for commit 576e89a. Configure here. |
||
| deadline = time.monotonic() + CHROME_STARTUP_TIMEOUT_S | ||
| while time.monotonic() < deadline: | ||
| if _debugger_listening(port): | ||
| return | ||
| time.sleep(0.25) | ||
| raise RuntimeError(f"Chrome did not open debugging port {port} within {CHROME_STARTUP_TIMEOUT_S:.0f}s") | ||
|
|
||
|
|
||
| def _debugger_listening(port: int) -> bool: | ||
| try: | ||
| return httpx.get(f"http://127.0.0.1:{port}/json/version", timeout=2.0).status_code == 200 | ||
| except httpx.HTTPError: | ||
| return False | ||
|
|
||
|
|
||
| def _find_chrome() -> str | None: | ||
| for path in _CHROME_CANDIDATES.get(platform.system(), ()): | ||
| if Path(path).exists(): | ||
| return path | ||
| for command in _CHROME_COMMANDS: | ||
| found = shutil.which(command) | ||
| if found: | ||
| return found | ||
| return None | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import os | ||
| import uuid as _uuid | ||
| from typing import Any | ||
|
|
||
| from pydantic import BaseModel, Field, model_validator | ||
| from typing_extensions import Self | ||
|
|
||
| from ..environment import HaiAgentsEnvironment | ||
|
|
||
| DEFAULT_BASE_URL = HaiAgentsEnvironment.EU.value | ||
| KIND_TO_CAPABILITY = {"web": "browser", "desktop": "desktop"} | ||
| CAPABILITIES = frozenset(KIND_TO_CAPABILITY.values()) | ||
|
|
||
|
|
||
| def session_id_from_environment_id(environment_id: str, api_key: str, capability: str) -> str: | ||
| return str(_uuid.uuid5(_uuid.NAMESPACE_DNS, f"{api_key}.{environment_id}.{capability}")) | ||
|
|
||
|
|
||
| class SidecarConfig(BaseModel): | ||
| capability: str | ||
| environment_id: str | ||
| api_key: str = "" | ||
| base_url: str = DEFAULT_BASE_URL | ||
| session_id: str = "" | ||
| driver_options: dict[str, Any] = Field(default_factory=dict) | ||
|
|
||
| @model_validator(mode="after") | ||
| def _resolve_defaults(self) -> Self: | ||
| if self.capability not in CAPABILITIES: | ||
| raise ValueError(f"unknown capability {self.capability!r}; expected one of {sorted(CAPABILITIES)}") | ||
| if not self.api_key: | ||
| self.api_key = os.getenv("HAI_API_KEY") or os.getenv("AGP_SERVICE_KEY") or os.getenv("AGP_API_KEY") or "" | ||
| if not self.api_key: | ||
| raise ValueError("api_key is required (or set HAI_API_KEY)") | ||
| if not self.session_id: | ||
| self.session_id = session_id_from_environment_id(self.environment_id, self.api_key, self.capability) | ||
| return self | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sidecar ignores API base URLMedium Severity
Reviewed by Cursor Bugbot for commit abf44a5. Configure here. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from .driver import LocalDesktopDriver, RunCommandResponse | ||
|
|
||
| __all__ = ["LocalDesktopDriver", "RunCommandResponse"] |


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Named agents skip sidecar startup
High Severity
With auto sidecars enabled,
create_sessiononly starts local sidecars from configs collected off the inlineagentpayload. A registered agent passed as a string (the usualagent="my-agent"flow) is returned unchanged bylocalize_agent, and catalog environment entries that are plain id strings never matchuser_devicein_local_target, socollect_sidecar_configsis empty and no sidecar is started.Additional Locations (2)
src/hai_agents/local/wiring.py#L21-L30src/hai_agents/local/wiring.py#L32-L40Reviewed by Cursor Bugbot for commit 4e8401e. Configure here.