From dcf6c3591c51ccded6018d43a3ff56a9949fc489 Mon Sep 17 00:00:00 2001 From: Zheng Tao Date: Sat, 20 Jun 2026 22:26:32 +0800 Subject: [PATCH] fix(whatsapp): add shared helper to resolve bridge dir with HERMES_HOME mirror Add resolve_whatsapp_bridge_dir() helper in whatsapp_common.py that: - Checks if install dir is writable - Mirrors bridge source to writable HERMES_HOME location if read-only - Returns the resolved path for both CLI installer and runtime adapter Wire into: - hermes_cli/main.py (CLI installer) - gateway/platforms/whatsapp.py (runtime adapter) Fixes #49561 --- gateway/platforms/whatsapp.py | 8 +++-- gateway/platforms/whatsapp_common.py | 53 ++++++++++++++++++++++++++++ hermes_cli/main.py | 4 +-- 3 files changed, 61 insertions(+), 4 deletions(-) diff --git a/gateway/platforms/whatsapp.py b/gateway/platforms/whatsapp.py index d649066268466..fc69fad5bb58f 100644 --- a/gateway/platforms/whatsapp.py +++ b/gateway/platforms/whatsapp.py @@ -260,11 +260,15 @@ class WhatsAppAdapter(WhatsAppBehaviorMixin, BasePlatformAdapter): share it. Only transport-specific code lives here. """ - # Default bridge location relative to the hermes-agent install - _DEFAULT_BRIDGE_DIR = Path(__file__).resolve().parents[2] / "scripts" / "whatsapp-bridge" + # Default bridge location resolved via shared helper + _DEFAULT_BRIDGE_DIR = None # resolved in __init__ def __init__(self, config: PlatformConfig): super().__init__(config, Platform.WHATSAPP) + # Use shared helper for bridge directory resolution (handles read-only install tree) + if WhatsAppAdapter._DEFAULT_BRIDGE_DIR is None: + from gateway.platforms.whatsapp_common import resolve_whatsapp_bridge_dir + WhatsAppAdapter._DEFAULT_BRIDGE_DIR = resolve_whatsapp_bridge_dir() self._bridge_process: Optional[subprocess.Popen] = None self._bridge_port: int = config.extra.get("bridge_port", 3000) self._bridge_script: Optional[str] = config.extra.get( diff --git a/gateway/platforms/whatsapp_common.py b/gateway/platforms/whatsapp_common.py index 6b56be3b8de9f..c6ed3da6e32b8 100644 --- a/gateway/platforms/whatsapp_common.py +++ b/gateway/platforms/whatsapp_common.py @@ -365,3 +365,56 @@ def _header_to_bold(m: re.Match) -> str: result = result.replace(f"{_CODE_PH}{i}\x00", code) return result + + +# --------------------------------------------------------------------------- +# Shared bridge directory resolution for CLI and adapter +# --------------------------------------------------------------------------- + +def resolve_whatsapp_bridge_dir() -> Path: + """Resolve the WhatsApp bridge directory, mirroring to HERMES_HOME if needed. + + When the install tree is read-only (e.g., Docker /opt/hermes), this function + mirrors the bridge source to a writable HERMES_HOME location and returns that + path. This ensures npm install works in Docker environments. + + Returns the resolved bridge directory path. + """ + import shutil + from pathlib import Path as _Path + + # Default location in install tree (may be read-only) + from hermes_constants import get_hermes_home + install_bridge = _Path(__file__).resolve().parents[2] / "scripts" / "whatsapp-bridge" + + # Try HERMES_HOME location first + hermes_home = get_hermes_home() + hermes_home_bridge = hermes_home / "scripts" / "whatsapp-bridge" + + # Check if install dir is writable + try: + test_file = install_bridge / ".write_test" + test_file.touch() + test_file.unlink() + install_writable = True + except (OSError, PermissionError): + install_writable = False + + if install_writable: + return install_bridge + + # Install dir is read-only, mirror to HERMES_HOME if needed + if hermes_home_bridge.exists(): + return hermes_home_bridge + + # Mirror the bridge source to HERMES_HOME + try: + hermes_home_bridge.parent.mkdir(parents=True, exist_ok=True) + shutil.copytree( + install_bridge, + hermes_home_bridge, + dirs_exist_ok=False, + ) + return hermes_home_bridge + except Exception: + return install_bridge diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 064b69277f6d8..ef6a176a21354 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -2466,8 +2466,8 @@ def cmd_whatsapp(args): print(" ⚠ No allowlist — the agent will respond to ALL incoming messages") # ── Step 4: Install bridge dependencies ────────────────────────────── - project_root = Path(__file__).resolve().parents[1] - bridge_dir = project_root / "scripts" / "whatsapp-bridge" + from gateway.platforms.whatsapp_common import resolve_whatsapp_bridge_dir + bridge_dir = resolve_whatsapp_bridge_dir() bridge_script = bridge_dir / "bridge.js" if not bridge_script.exists():