Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 16 additions & 11 deletions hermes_cli/web_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,17 +282,6 @@ def _get_pty_active_session_files(app: "FastAPI") -> dict[str, Path]:
_REVEAL_MAX_PER_WINDOW = 5
_REVEAL_WINDOW_SECONDS = 30

# CORS: restrict to localhost origins only. The web UI is intended to run
# locally; binding to 0.0.0.0 with allow_origins=["*"] would let any website
# read/modify config and secrets.

app.add_middleware(
CORSMiddleware,
allow_origin_regex=r"^https?://(localhost|127\.0\.0\.1)(:\d+)?$",
allow_methods=["*"],
allow_headers=["*"],
)

# ---------------------------------------------------------------------------
# Endpoints that do NOT require the session token. Everything else under
# /api/ is gated by the auth middleware below.
Expand Down Expand Up @@ -600,6 +589,22 @@ async def _token_auth_seam(request: Request, call_next):
return await token_auth_middleware(request, call_next)


# CORS: restrict to localhost origins only. The web UI is intended to run
# locally; binding to 0.0.0.0 with allow_origins=["*"] would let any website
# read/modify config and secrets.
#
# Registered AFTER all auth middlewares so it is the outermost (runs first
# on incoming requests) in the Starlette middleware stack. This ensures
# OPTIONS preflight requests to /api/* get CORS headers without hitting
# the auth gate — the bug that motivated issue #59052.
app.add_middleware(
CORSMiddleware,
allow_origin_regex=r"^https?://(localhost|127\.0\.0\.1)(:\d+)?$",
allow_methods=["*"],
allow_headers=["*"],
)


# ---------------------------------------------------------------------------
# Config schema — auto-generated from DEFAULT_CONFIG
# ---------------------------------------------------------------------------
Expand Down
19 changes: 18 additions & 1 deletion plugins/observability/langfuse/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,23 @@
This plugin ships bundled with Hermes but is **opt-in** — it only loads when
you explicitly enable it.

> **⚠️ WARNING: SDK dependency can silently disappear after updates**
>
> The `langfuse` Python SDK (`pip install langfuse`) is **required** for tracing
> to work. If the SDK is missing from the active Hermes environment, tracing
> silently stops — the plugin fails open and produces no output or errors.
>
> After a Hermes update (`pip install --upgrade hermes-agent`), a `venv` refresh,
> or a reinstall, the `langfuse` SDK may be removed from the environment, causing
> tracing to silently stop. Always reinstall the SDK after such operations:
>
> ```bash
> pip install langfuse
> ```
>
> The plugin logs a one-time warning at startup if the SDK is missing; check
> your logs if you suspect tracing has stopped.

## Enable

Pick one:
Expand All @@ -27,7 +44,7 @@ HERMES_LANGFUSE_BASE_URL=https://cloud.langfuse.com # or your self-hosted URL
```

Without the SDK or credentials the hooks no-op silently — the plugin fails
open.
open. See the **⚠️ warning above** about the SDK dependency.

## Verify

Expand Down
6 changes: 6 additions & 0 deletions plugins/observability/langfuse/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,12 @@ def _get_langfuse() -> Optional[Langfuse]:
return _LANGFUSE_CLIENT

if Langfuse is None:
logger.warning(
"Langfuse plugin: the 'langfuse' SDK is not installed — tracing will be "
"silently disabled. Install it with: pip install langfuse. "
"After a Hermes update or venv refresh the SDK may be removed; reinstall "
"it to restore tracing."
)
_LANGFUSE_CLIENT = _INIT_FAILED
return None

Expand Down
5 changes: 5 additions & 0 deletions plugins/observability/langfuse/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Required: Langfuse Python SDK for observability tracing.
# If this SDK is missing from the Hermes environment, tracing silently stops.
# Reinstall after any Hermes update or venv refresh:
# pip install langfuse
langfuse>=2.36,<3.0
32 changes: 16 additions & 16 deletions tools/browser_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ def _build_browser_env() -> dict:
try:
from tools.website_policy import check_website_access
except Exception:
check_website_access = lambda url: None # noqa: E731 — fail-open if policy module unavailable
check_website_access = lambda url: None

try:
from tools.url_safety import (
Expand All @@ -117,26 +117,26 @@ def _build_browser_env() -> dict:
sensitive_query_param_name as _sensitive_query_param_name,
)
except Exception:
_is_safe_url = lambda url: False # noqa: E731 — fail-closed: block all if safety module unavailable
_is_always_blocked_url = lambda url: True # noqa: E731 — fail-closed on the floor too
_normalize_url_for_request = lambda url: url # noqa: E731 — best-effort fallback
_sensitive_query_param_name = lambda url: None # noqa: E731 — best-effort fallback
_is_safe_url = lambda url: False
_is_always_blocked_url = lambda url: True
_normalize_url_for_request = lambda url: url
_sensitive_query_param_name = lambda url: None
# Browser-provider ABC + registry — PR #25214 moved the per-vendor providers
# (Browserbase / Browser Use / Firecrawl) out of ``tools/browser_providers/``
# and into ``plugins/browser/<vendor>/``. The dispatcher consults the
# registry; the legacy class names are re-exported below as backward-compat
# shims for callers that import them from this module.
from agent.browser_provider import BrowserProvider as CloudBrowserProvider # noqa: F401 (legacy alias)
from agent.browser_registry import ( # noqa: F401 (test-patchable surface)
from agent.browser_provider import BrowserProvider as CloudBrowserProvider
from agent.browser_registry import (
get_provider as _registry_get_browser_provider,
)
from plugins.browser.browserbase.provider import ( # noqa: F401 (legacy import surface)
from plugins.browser.browserbase.provider import (
BrowserbaseBrowserProvider as BrowserbaseProvider,
)
from plugins.browser.browser_use.provider import ( # noqa: F401
from plugins.browser.browser_use.provider import (
BrowserUseBrowserProvider as BrowserUseProvider,
)
from plugins.browser.firecrawl.provider import ( # noqa: F401
from plugins.browser.firecrawl.provider import (
FirecrawlBrowserProvider as FirecrawlProvider,
)
from tools.tool_backend_helpers import normalize_browser_cloud_provider
Expand All @@ -146,7 +146,7 @@ def _build_browser_env() -> dict:
try:
from tools.browser_camofox import is_camofox_mode as _is_camofox_mode
except ImportError:
_is_camofox_mode = lambda: False # noqa: E731
_is_camofox_mode = lambda: False

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -543,7 +543,7 @@ def _ensure_cdp_supervisor(task_id: str) -> None:
if not cdp_url:
return
try:
from tools.browser_supervisor import SUPERVISOR_REGISTRY # type: ignore[import-not-found]
from tools.browser_supervisor import SUPERVISOR_REGISTRY

policy, timeout_s = _get_dialog_policy_config()
SUPERVISOR_REGISTRY.get_or_start(
Expand All @@ -563,7 +563,7 @@ def _ensure_cdp_supervisor(task_id: str) -> None:
def _stop_cdp_supervisor(task_id: str) -> None:
"""Stop the CDP supervisor for ``task_id`` if one exists. No-op otherwise."""
try:
from tools.browser_supervisor import SUPERVISOR_REGISTRY # type: ignore[import-not-found]
from tools.browser_supervisor import SUPERVISOR_REGISTRY

SUPERVISOR_REGISTRY.stop(task_id)
except Exception as exc:
Expand Down Expand Up @@ -2999,7 +2999,7 @@ def browser_snapshot(
# supervisor is attached to this task. No-op otherwise. See
# website/docs/developer-guide/browser-supervisor.md.
try:
from tools.browser_supervisor import SUPERVISOR_REGISTRY # type: ignore[import-not-found]
from tools.browser_supervisor import SUPERVISOR_REGISTRY
_supervisor = SUPERVISOR_REGISTRY.get(effective_task_id)
if _supervisor is not None:
_sv_snap = _supervisor.snapshot()
Expand Down Expand Up @@ -3571,7 +3571,7 @@ def _browser_eval(expression: str, task_id: Optional[str] = None) -> str:
# subprocess path on any error so behaviour is unchanged when no
# supervisor is running (e.g. plain agent-browser without a CDP backend).
try:
from tools.browser_supervisor import SUPERVISOR_REGISTRY # type: ignore[import-not-found]
from tools.browser_supervisor import SUPERVISOR_REGISTRY
supervisor = SUPERVISOR_REGISTRY.get(effective_task_id)
if supervisor is not None:
sup_result = supervisor.evaluate_runtime(expression)
Expand Down Expand Up @@ -4372,7 +4372,7 @@ def cleanup_all_browsers() -> None:

# Tear down CDP supervisors for all tasks so background threads exit.
try:
from tools.browser_supervisor import SUPERVISOR_REGISTRY # type: ignore[import-not-found]
from tools.browser_supervisor import SUPERVISOR_REGISTRY
SUPERVISOR_REGISTRY.stop_all()
except Exception:
pass
Expand Down
Loading