From ed7c535ebc7a03e35bed609eb99580aef915eb9f Mon Sep 17 00:00:00 2001 From: Tranquil-Flow <66773372+Tranquil-Flow@users.noreply.github.com> Date: Wed, 9 Sep 2026 22:10:41 +0200 Subject: [PATCH] fix(gateway): tolerate unresolvable UIDs in launchd plist path lookup (#57292) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- hermes_cli/gateway.py | 12 +++- tests/hermes_cli/test_gateway_service.py | 84 ++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 2 deletions(-) diff --git a/hermes_cli/gateway.py b/hermes_cli/gateway.py index 02805b5cab..8bd8a7bff1 100644 --- a/hermes_cli/gateway.py +++ b/hermes_cli/gateway.py @@ -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" diff --git a/tests/hermes_cli/test_gateway_service.py b/tests/hermes_cli/test_gateway_service.py index 0698a70d42..1bae3db5f2 100644 --- a/tests/hermes_cli/test_gateway_service.py +++ b/tests/hermes_cli/test_gateway_service.py @@ -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()."""