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:
@@ -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"
|
||||
|
||||
|
||||
|
||||
@@ -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()."""
|
||||
|
||||
Reference in New Issue
Block a user