Bridge install failures in read-only/container environments
Cluster 49561 · environments. Data as of
1 open item. Fix merged in #73864 on 29 Jul 2026. Check it covers your case.
The short answer
- Known?
- Yes. 10 related items grouped here, 1 issue already closed.
- Fix event
- A fix was merged to main in PR #73864. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
partially resolved, deciding PR #49839. That is separate from the fix event. - Still open
- 1 issue and 0 PRs.
Next step: read #73864, then check whether your report is the one still open.
Read #73864Pick up a Lab taskThese issues and fixes address failures installing WhatsApp and iMessage bridges when running in Docker, Podman, or other environments with read-only or rootless filesystem constraints. Work centers on writable directory resolution, HERMES_HOME handling, and npm permission compatibility.
Open issues1
- Issue#62975[Bug]: Node sidecar dep (NPM) error on Podman install
Open PRs0
Fixes on record
-
#49839 fix(whatsapp): resolve bridge dir with HERMES_HOME mirror in read-only Docker (fix #49561)
files:
gateway/platforms/whatsapp_common.py,hermes_cli/main.py,plugins/platforms/whatsapp/adapter.py,scripts/release.py,tests/gateway/test_whatsapp_bridge_dir_resolution.pyShow the diff
-
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 -
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(): -
plugins/platforms/whatsapp/adapter.py@@ -261,11 +261,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( -
scripts/release.py@@ -477,6 +477,7 @@ "[redacted:email]": "Krionex", "[email protected]": "rxdxxxx", "[redacted:email]": "MaHaoHao-ch", + "[redacted:email]": "xydigit-zt", "[email protected]": "revaraver", "[redacted:email]": "TheEpTic", "[email protected]": "wmagev", -
tests/gateway/test_whatsapp_bridge_dir_resolution.py@@ -0,0 +1,120 @@ +"""Tests for resolve_whatsapp_bridge_dir() — read-only install tree handling. + +Regression coverage for #49561: in the Docker image the install tree +(/opt/hermes/scripts/whatsapp-bridge) is read-only, so `npm install` fails +with EACCES. The resolver must detect the read-only install dir and mirror the +bridge source into a writable HERMES_HOME location instead. +""" +import importlib +from pathlib import Path + +import pytest + +from gateway.platforms import whatsapp_common + + +def _seed_install_tree(install_bridge: Path) -> None: + """Create a minimal fake bridge source tree.""" + install_bridge.mkdir(parents=True, exist_ok=True) + (install_bridge / "bridge.js").write_text("// bridge\n") + (install_bridge / "package.json").write_text('{"name": "whatsapp-bridge"}\n') + + +def test_writable_install_returns_install_dir(tmp_path, monkeypatch): + """When the install tree is writable, the resolver returns it unchanged.""" + install_root = tmp_path / "install" + install_bridge = install_root / "scripts" / "whatsapp-bridge" + _seed_install_tree(install_bridge) + + hermes_home = tmp_path / "hermes_home" + hermes_home.mkdir() + + # Point the resolver's two anchors at our temp dirs. + monkeypatch.setattr( + whatsapp_common, "__file__", + str(install_root / "gateway" / "platforms" / "whatsapp_common.py"), + ) + monkeypatch.setattr( + "hermes_constants.get_hermes_home", lambda: hermes_home + ) + + resolved = whatsapp_common.resolve_whatsapp_bridge_dir() + assert resolved == install_bridge + # Nothing mirrored into HERMES_HOME. + assert not (hermes_home / "scripts" / "whatsapp-bridge").exists() + + +def test_readonly_install_mirrors_to_hermes_home(tmp_path, monkeypatch): + """A read-only install tree is mirrored into a writable HERMES_HOME.""" + install_root = tmp_path / "install" + install_bridge = install_root / "scripts" / "whatsapp-bridge" + _seed_install_tree(install_bridge) + + hermes_home = tmp_path / "hermes_home" + hermes_home.mkdir() + + monkeypatch.setattr( + whatsapp_common, "__file__", + str(install_root / "gateway" / "platforms" / "whatsapp_common.py"), + ) + monkeypatch.setattr( + "hermes_constants.get_hermes_home", lambda: hermes_home + ) + + # Simulate a read-only install tree. chmod(0o555) is unreliable under + # root (CI/Docker bypass permission bits), so force the write probe to + # fail by raising on the .write_test touch for the install dir only. + _real_touch = Path.touch + + def _fake_touch(self, *a, **kw): + if self.name == ".write_test" and install_bridge in self.parents: + raise PermissionError("read-only install tree") + return _real_touch(self, *a, **kw) + + monkeypatch.setattr(Path, "touch", _fake_touch) + + resolved = whatsapp_common.resolve_whatsapp_bridge_dir() + + expected = hermes_home / "scripts" / "whatsapp-bridge" + assert resolved == expected + # Source was mirrored, not symlinked. + assert (expected / "bridge.js").read_text() == "// bridge\n" + assert (expected / "package.json").exists() + + +def test_readonly_install_reuses_existing_mirror(tmp_path, monkeypatch): + """If the HERMES_HOME mirror already exists, return it without re-copying.""" + install_root = tmp_path / "install" + install_bridge = install_root / "scripts" / "whatsapp-bridge" + _seed_install_tree(install_bridge) + + hermes_home = tmp_path / "hermes_home" + mirror = hermes_home / "scripts" / "whatsapp-bridge" + mirror.mkdir(parents=True) + # A sentinel file proves the resolver returned the EXISTING mirror + # rather than wiping/recopying it. + (mirror / "node_modules").mkdir() + (mirror / "node_modules" / "sentinel").write_text("keep me\n") + + monkeypatch.setattr( + whatsapp_common, "__file__", + str(install_root / "gateway" / "platforms" / "whatsapp_common.py"), + ) + monkeypatch.setattr( + "hermes_constants.get_hermes_home", lambda: hermes_home + ) + + _real_touch = Path.touch + + def _fake_touch(self, *a, **kw): + if self.name == ".write_test" and install_bridge in self.parents: + raise PermissionError("read-only install tree") + return _real_touch(self, *a, **kw) + + monkeypatch.setattr(Path, "touch", _fake_touch) + + resolved = whatsapp_common.resolve_whatsapp_bridge_dir() + + assert resolved == mirror + # Existing node_modules left intact (no destructive re-copy). + assert (mirror / "node_modules" / "sentinel").read_text() == "keep me\n"
-
-
#73864 fix(photon): immutable install trees for the iMessage sidecar (NS-606) [salvage #69910]
files:
Dockerfile,plugins/platforms/photon/adapter.py,plugins/platforms/photon/cli.py,plugins/platforms/photon/sidecar_paths.py,tests/plugins/platforms/photon/test_check_requirements_risks.py,tests/plugins/platforms/photon/test_npm_error_log_regression.py,tests/plugins/platforms/photon/test_sidecar_lifecycle.py,tests/plugins/platforms/photon/test_sidecar_paths.py,tests/tools/test_dockerfile_immutable_install.py
6 PRs closed without merging
From the close comments: superseded by another PR 2 · merged via another change 1 · other 3
-
#49584 fix(whatsapp): use writable HERMES_HOME for bridge in Docker (fix #49561) · other →
#49557,#49599,#49561 -
#49599 fix(whatsapp): install bridge into a writable dir on read-only installs (fix #49561) · other →
#49561,#49584,#49569 -
#49654 fix(whatsapp): resolve bridge dir with HERMES_HOME mirror (fix #49561) · merged via another change →
491579f,#49839 -
#62980 fix(photon): install sidecar deps under hermes home · superseded by another PR →
b62fc24,#73864,#69910 - #63906 fix(install): add --unsafe-perm to npm install for Podman/Docker rootless compat · other
Show 1 more
-
#73893 fix(photon): support immutable install trees for the iMessage sidecar (salvage #69910) · superseded by another PR →
b62fc24,#73864,#69910
Closed issues · 1
- Closed#49561[Bug]: Regression - WhatsApp bridge cannot be installed
How this cluster was grouped
-
Link evidence (
partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text. - Members outside that structural core may belong to a different underlying problem.
-
Pairwise link density
0.21: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the Lab