diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index 45e5d7b84966..bbfa85e9d2eb 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -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. @@ -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 # --------------------------------------------------------------------------- diff --git a/plugins/observability/langfuse/README.md b/plugins/observability/langfuse/README.md index 864735d96884..820e9177dd49 100644 --- a/plugins/observability/langfuse/README.md +++ b/plugins/observability/langfuse/README.md @@ -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: @@ -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 diff --git a/plugins/observability/langfuse/__init__.py b/plugins/observability/langfuse/__init__.py index 31904d47e305..e0dd21806e61 100644 --- a/plugins/observability/langfuse/__init__.py +++ b/plugins/observability/langfuse/__init__.py @@ -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 diff --git a/plugins/observability/langfuse/requirements.txt b/plugins/observability/langfuse/requirements.txt new file mode 100644 index 000000000000..a2f7e2eedf30 --- /dev/null +++ b/plugins/observability/langfuse/requirements.txt @@ -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 diff --git a/tools/browser_tool.py b/tools/browser_tool.py index 82c248a3f715..9ceebc56e19e 100644 --- a/tools/browser_tool.py +++ b/tools/browser_tool.py @@ -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 ( @@ -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//``. 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 @@ -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__) @@ -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( @@ -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: @@ -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() @@ -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) @@ -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