fix(gateway): tolerate unresolvable UIDs in launchd plist path lookup (#57292)

Sandboxed/app-hosted macOS shells can expose a UID that pwd.getpwuid
cannot resolve, crashing every launchd-dependent gateway command
(install/status/restart) at the raw getpwuid call inlined into
get_launchd_plist_path() by the service-kind refactor — the same crash
the pre-refactor _launchd_user_home() had.

Keep pwd authoritative when it resolves (profile-home correctness
unchanged) and fall back to the established get_real_home() resolver
(HERMES_REAL_HOME → HOME → pwd → ~, profile home skipped) on
KeyError/ImportError/OSError, so launchd commands degrade gracefully
instead of crashing. Covers both reported crash sites (install path and
_installed_service_kind_for restart path) and all sibling callers.
This commit is contained in:
Tranquil-Flow
2026-09-09 22:10:41 +02:00
committed by brooklyn!
parent f4a9bcab98
commit ed7c535ebc
2 changed files with 94 additions and 2 deletions

View File

@@ -2923,8 +2923,16 @@ def get_launchd_plist_path() -> Path:
import pwd
suffix = _profile_suffix()
name = f"ai.hermes.gateway-{suffix}" if suffix else "ai.hermes.gateway"
# Real account home: profile mode may point HOME at a profile dir.
home = Path(pwd.getpwuid(os.getuid()).pw_dir) # windows-footgun: ok — POSIX launchd (macOS) helper, never invoked on Windows
# Real account home: profile mode may point HOME at a profile dir. Sandboxed/app-hosted
# shells can expose a UID that pwd cannot resolve (#57292); fall back to the shared
# real-home resolver (HERMES_REAL_HOME → HOME → pwd → ~, profile home skipped) instead
# of crashing launchd commands.
try:
home = Path(pwd.getpwuid(os.getuid()).pw_dir) # windows-footgun: ok — POSIX launchd (macOS) helper, never invoked on Windows
except (KeyError, ImportError, OSError):
from hermes_constants import get_real_home
home = Path(get_real_home())
return home / "Library" / "LaunchAgents" / f"{name}.plist"

View File

@@ -1870,6 +1870,90 @@ class TestProfileArg:
assert plist_path == machine_home / "Library" / "LaunchAgents" / "ai.hermes.gateway-orcha.plist"
def test_launchd_plist_path_falls_back_to_home_when_uid_lookup_fails(self, tmp_path, monkeypatch):
"""Sandboxed macOS shells can expose a UID that pwd cannot resolve (#57292)."""
profile_dir = tmp_path / ".hermes" / "profiles" / "mybot"
profile_dir.mkdir(parents=True)
machine_home = tmp_path / "Users" / "example"
machine_home.mkdir(parents=True)
monkeypatch.setenv("HERMES_HOME", str(profile_dir))
monkeypatch.delenv("HERMES_REAL_HOME", raising=False)
monkeypatch.setenv("HOME", str(machine_home))
monkeypatch.setattr(gateway_cli, "get_hermes_home", lambda: profile_dir)
def raise_key_error(uid):
raise KeyError(f"getpwuid(): uid not found: {uid}")
monkeypatch.setattr(pwd, "getpwuid", raise_key_error)
plist_path = gateway_cli.get_launchd_plist_path()
assert plist_path == machine_home / "Library" / "LaunchAgents" / "ai.hermes.gateway-mybot.plist"
def test_launchd_plist_path_prefers_hermes_real_home_when_uid_lookup_fails(self, tmp_path, monkeypatch):
"""HERMES_REAL_HOME is the explicit operator override for unresolvable UIDs (#57292)."""
profile_dir = tmp_path / ".hermes" / "profiles" / "mybot"
profile_dir.mkdir(parents=True)
real_home = tmp_path / "real-home"
other_home = tmp_path / "other-home"
real_home.mkdir(parents=True)
other_home.mkdir(parents=True)
monkeypatch.setenv("HERMES_HOME", str(profile_dir))
monkeypatch.setenv("HERMES_REAL_HOME", str(real_home))
monkeypatch.setenv("HOME", str(other_home))
monkeypatch.setattr(gateway_cli, "get_hermes_home", lambda: profile_dir)
def raise_key_error(uid):
raise KeyError(f"getpwuid(): uid not found: {uid}")
monkeypatch.setattr(pwd, "getpwuid", raise_key_error)
plist_path = gateway_cli.get_launchd_plist_path()
assert plist_path == real_home / "Library" / "LaunchAgents" / "ai.hermes.gateway-mybot.plist"
def test_launchd_plist_path_fallback_never_returns_profile_home(self, tmp_path, monkeypatch):
"""When pwd fails and HOME IS the profile home, the fallback must skip it (#57292)."""
profile_dir = tmp_path / ".hermes" / "profiles" / "mybot"
profile_dir.mkdir(parents=True)
profile_home = profile_dir / "home"
profile_home.mkdir(parents=True)
monkeypatch.setenv("HERMES_HOME", str(profile_dir))
monkeypatch.delenv("HERMES_REAL_HOME", raising=False)
monkeypatch.setenv("HOME", str(profile_home))
monkeypatch.setattr(gateway_cli, "get_hermes_home", lambda: profile_dir)
def raise_key_error(uid):
raise KeyError(f"getpwuid(): uid not found: {uid}")
monkeypatch.setattr(pwd, "getpwuid", raise_key_error)
plist_path = gateway_cli.get_launchd_plist_path()
assert profile_home not in plist_path.parents
assert profile_dir not in plist_path.parents
def test_installed_service_kind_returns_none_when_uid_unresolvable(self, tmp_path, monkeypatch):
"""`gateway restart`/`status` degrade to "not installed" instead of crashing (#57292)."""
machine_home = tmp_path / "Users" / "example"
machine_home.mkdir(parents=True)
monkeypatch.setenv("HERMES_HOME", str(tmp_path / ".hermes"))
monkeypatch.delenv("HERMES_REAL_HOME", raising=False)
monkeypatch.setenv("HOME", str(machine_home))
def raise_key_error(uid):
raise KeyError(f"getpwuid(): uid not found: {uid}")
monkeypatch.setattr(pwd, "getpwuid", raise_key_error)
monkeypatch.setattr(gateway_cli, "is_macos", lambda: True)
monkeypatch.setattr(gateway_cli, "_systemd_unit_installed", lambda: False)
assert gateway_cli._installed_service_kind_for(lambda: False) is None
class TestRemapPathForUser:
"""Unit tests for _remap_path_for_user()."""