mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-09 08:21:50 +00:00
Adds two Camofox features: 1. Persistent browser sessions: new `browser.camofox.managed_persistence` config option. When enabled, Hermes sends a deterministic profile-scoped userId to Camofox so the server maps it to a persistent browser profile directory. Cookies, logins, and browser state survive across restarts. Default remains ephemeral (random userId per session). 2. VNC URL discovery: Camofox /health endpoint returns vncPort when running in headed mode. Hermes constructs the VNC URL and includes it in navigate responses so the agent can share it with users. Also fixes camofox_vision bug where call_llm response object was passed directly to json.dumps instead of extracting .choices[0].message.content. Changes from original PR: - Removed browser_evaluate tool (separate feature, needs own PR) - Removed snapshot truncation limit change (unrelated) - Config.yaml only for managed_persistence (no env var, no version bump) - Rewrote tests to use config mock instead of env var - Reverted package-lock.json churn Co-authored-by: analista <psikonetik@gmail.com.com>
This commit is contained in:
parent
85e96a4638
commit
70744add15
8 changed files with 485 additions and 10 deletions
|
|
@ -34,6 +34,9 @@ from typing import Any, Dict, Optional
|
|||
|
||||
import requests
|
||||
|
||||
from hermes_cli.config import load_config
|
||||
from tools.browser_camofox_state import get_camofox_identity
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -42,6 +45,8 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
_DEFAULT_TIMEOUT = 30 # seconds per HTTP request
|
||||
_SNAPSHOT_MAX_CHARS = 80_000 # camofox paginates at this limit
|
||||
_vnc_url: Optional[str] = None # cached from /health response
|
||||
_vnc_url_checked = False # only probe once per process
|
||||
|
||||
|
||||
def get_camofox_url() -> str:
|
||||
|
|
@ -56,16 +61,52 @@ def is_camofox_mode() -> bool:
|
|||
|
||||
def check_camofox_available() -> bool:
|
||||
"""Verify the Camofox server is reachable."""
|
||||
global _vnc_url, _vnc_url_checked
|
||||
url = get_camofox_url()
|
||||
if not url:
|
||||
return False
|
||||
try:
|
||||
resp = requests.get(f"{url}/health", timeout=5)
|
||||
if resp.status_code == 200 and not _vnc_url_checked:
|
||||
try:
|
||||
data = resp.json()
|
||||
vnc_port = data.get("vncPort")
|
||||
if isinstance(vnc_port, int) and 1 <= vnc_port <= 65535:
|
||||
from urllib.parse import urlparse
|
||||
parsed = urlparse(url)
|
||||
host = parsed.hostname or "localhost"
|
||||
_vnc_url = f"http://{host}:{vnc_port}"
|
||||
except (ValueError, KeyError):
|
||||
pass
|
||||
_vnc_url_checked = True
|
||||
return resp.status_code == 200
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def get_vnc_url() -> Optional[str]:
|
||||
"""Return the VNC URL if the Camofox server exposes one, or None."""
|
||||
if not _vnc_url_checked:
|
||||
check_camofox_available()
|
||||
return _vnc_url
|
||||
|
||||
|
||||
def _managed_persistence_enabled() -> bool:
|
||||
"""Return whether Hermes-managed persistence is enabled for Camofox.
|
||||
|
||||
When enabled, sessions use a stable profile-scoped userId so the
|
||||
Camofox server can map it to a persistent browser profile directory.
|
||||
When disabled (default), each session gets a random userId (ephemeral).
|
||||
|
||||
Controlled by ``browser.camofox.managed_persistence`` in config.yaml.
|
||||
"""
|
||||
try:
|
||||
camofox_cfg = load_config().get("browser", {}).get("camofox", {})
|
||||
except Exception:
|
||||
return False
|
||||
return bool(camofox_cfg.get("managed_persistence"))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Session management
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -75,16 +116,31 @@ _sessions_lock = threading.Lock()
|
|||
|
||||
|
||||
def _get_session(task_id: Optional[str]) -> Dict[str, Any]:
|
||||
"""Get or create a camofox session for the given task."""
|
||||
"""Get or create a camofox session for the given task.
|
||||
|
||||
When managed persistence is enabled, uses a deterministic userId
|
||||
derived from the Hermes profile so the Camofox server can map it
|
||||
to the same persistent browser profile across restarts.
|
||||
"""
|
||||
task_id = task_id or "default"
|
||||
with _sessions_lock:
|
||||
if task_id in _sessions:
|
||||
return _sessions[task_id]
|
||||
session = {
|
||||
"user_id": f"hermes_{uuid.uuid4().hex[:10]}",
|
||||
"tab_id": None,
|
||||
"session_key": f"task_{task_id[:16]}",
|
||||
}
|
||||
if _managed_persistence_enabled():
|
||||
identity = get_camofox_identity(task_id)
|
||||
session = {
|
||||
"user_id": identity["user_id"],
|
||||
"tab_id": None,
|
||||
"session_key": identity["session_key"],
|
||||
"managed": True,
|
||||
}
|
||||
else:
|
||||
session = {
|
||||
"user_id": f"hermes_{uuid.uuid4().hex[:10]}",
|
||||
"tab_id": None,
|
||||
"session_key": f"task_{task_id[:16]}",
|
||||
"managed": False,
|
||||
}
|
||||
_sessions[task_id] = session
|
||||
return session
|
||||
|
||||
|
|
@ -172,11 +228,19 @@ def camofox_navigate(url: str, task_id: Optional[str] = None) -> str:
|
|||
{"userId": session["user_id"], "url": url},
|
||||
timeout=60,
|
||||
)
|
||||
return json.dumps({
|
||||
result = {
|
||||
"success": True,
|
||||
"url": data.get("url", url),
|
||||
"title": data.get("title", ""),
|
||||
})
|
||||
}
|
||||
vnc = get_vnc_url()
|
||||
if vnc:
|
||||
result["vnc_url"] = vnc
|
||||
result["vnc_hint"] = (
|
||||
"Browser is visible via VNC. "
|
||||
"Share this link with the user so they can watch the browser live."
|
||||
)
|
||||
return json.dumps(result)
|
||||
except requests.HTTPError as e:
|
||||
return json.dumps({"success": False, "error": f"Navigation failed: {e}"})
|
||||
except requests.ConnectionError:
|
||||
|
|
@ -436,7 +500,7 @@ def camofox_vision(question: str, annotate: bool = False,
|
|||
except Exception:
|
||||
_vision_timeout = 120
|
||||
|
||||
analysis = call_llm(
|
||||
response = call_llm(
|
||||
messages=[{
|
||||
"role": "user",
|
||||
"content": [
|
||||
|
|
@ -452,6 +516,7 @@ def camofox_vision(question: str, annotate: bool = False,
|
|||
task="vision",
|
||||
timeout=_vision_timeout,
|
||||
)
|
||||
analysis = response.choices[0].message.content if response.choices else ""
|
||||
|
||||
return json.dumps({
|
||||
"success": True,
|
||||
|
|
|
|||
47
tools/browser_camofox_state.py
Normal file
47
tools/browser_camofox_state.py
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
"""Hermes-managed Camofox state helpers.
|
||||
|
||||
Provides profile-scoped identity and state directory paths for Camofox
|
||||
persistent browser profiles. When managed persistence is enabled, Hermes
|
||||
sends a deterministic userId derived from the active profile so that
|
||||
Camofox can map it to the same persistent browser profile directory
|
||||
across restarts.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional
|
||||
|
||||
from hermes_constants import get_hermes_home
|
||||
|
||||
CAMOFOX_STATE_DIR_NAME = "browser_auth"
|
||||
CAMOFOX_STATE_SUBDIR = "camofox"
|
||||
|
||||
|
||||
def get_camofox_state_dir() -> Path:
|
||||
"""Return the profile-scoped root directory for Camofox persistence."""
|
||||
return get_hermes_home() / CAMOFOX_STATE_DIR_NAME / CAMOFOX_STATE_SUBDIR
|
||||
|
||||
|
||||
def get_camofox_identity(task_id: Optional[str] = None) -> Dict[str, str]:
|
||||
"""Return the stable Hermes-managed Camofox identity for this profile.
|
||||
|
||||
The user identity is profile-scoped (same Hermes profile = same userId).
|
||||
The session key is scoped to the logical browser task so newly created
|
||||
tabs within the same profile reuse the same identity contract.
|
||||
"""
|
||||
scope_root = str(get_camofox_state_dir())
|
||||
logical_scope = task_id or "default"
|
||||
user_digest = uuid.uuid5(
|
||||
uuid.NAMESPACE_URL,
|
||||
f"camofox-user:{scope_root}",
|
||||
).hex[:10]
|
||||
session_digest = uuid.uuid5(
|
||||
uuid.NAMESPACE_URL,
|
||||
f"camofox-session:{scope_root}:{logical_scope}",
|
||||
).hex[:16]
|
||||
return {
|
||||
"user_id": f"hermes_{user_digest}",
|
||||
"session_key": f"task_{session_digest}",
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue