NousResearch/hermes-agent

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

1open issue
0open PRs
10items grouped
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.

These 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

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #49839 fix(whatsapp): resolve bridge dir with HERMES_HOME mirror in read-only Docker (fix #49561)

    merged to main

    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.py

    Show 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]

    merged to main

    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.00 means 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

At a glance

Subsystem
environments
Merged PRs
#49839, #73864
Closed issues
1
Attempts
6 PRs closed without merging
Fix link checked
29 Jul 2026

For your agent

The same facts, typed.