From 4e2bd09229e161fbbe79ea34ffbf0f6389138538 Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Mon, 28 Sep 2026 00:48:39 -0700 Subject: [PATCH] fix(gateway): adopt a bare legacy user unit that pins this custom HERMES_HOME (#109476) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After the native-identity change for custom roots (#105525 / PR #106611) a custom HERMES_HOME names its service `hermes-gateway-<8hex>`, but an install made before that change left the bare `hermes-gateway.service` user unit pinning that same root. `_systemd_unit_installed()` only probed the recomputed name, so `gateway restart/stop/status` saw no unit: `restart` fell through to a foreground `run_gateway()` while the still-enabled legacy unit's Restart=always looped on the instance lock ("Gateway already running") and the shell hung on the foreground gateway. `_bare_unit_pinned_home()` already made an installed bare SYSTEM unit the naming authority for root; the user scope now does the same with its own bare USER unit. A unit whose pinned HERMES_HOME resolves to this home is this home's service whatever it is named, so every lifecycle command (restart, stop, status, install-in-place, uninstall) discovers and adopts it. The match is exact-home only: a bare unit pinning another home — including the production default from a temp-home harness — still yields this home's own suffix, and an unprivileged process never reads the system unit (the profile-aliasing hazard the root gate existed for). No new legacy name is added to `_LEGACY_SERVICE_NAMES`: that sweep removes units, and the bare unit is the correct unit for a default home. WHEN L5 (lifecycle commands run against an env-bound home) / WHERE T4 (user systemd unit), T1. (cherry picked from commit 037ff16bea7d1e4ce513543ebd14320ccf49b1a3) --- hermes_cli/gateway.py | 24 ++++++++++------- tests/hermes_cli/test_gateway_unit_paths.py | 30 +++++++++++++++++++++ 2 files changed, 45 insertions(+), 9 deletions(-) diff --git a/hermes_cli/gateway.py b/hermes_cli/gateway.py index b5f8bb704f..2e75b92234 100644 --- a/hermes_cli/gateway.py +++ b/hermes_cli/gateway.py @@ -2221,17 +2221,23 @@ def _bare_unit_pinned_home() -> Path | None: one naming basis that holds still across the sudo mid-command switch (see ``_profile_suffix``) and it covers every elevated identity — ``sudo -i`` and cron included, where SUDO_USER is absent. - Linux- and root-gated: a systemd unit is not an identity authority for launchd labels, Windows - scheduled tasks, or s6 slots, which share ``_profile_suffix()``, and only an elevated process ever - operates the system unit — an unprivileged user-scope command must keep naming its own units, or a - bare system unit pinning ``profiles/`` would alias that profile onto the user's default unit. - ``is_linux()`` is a plain ``sys.platform`` test; ``supports_systemd_services()`` would be wrong here, - since it can shell out to ``systemctl is-system-running`` on WSL/containers and this runs on every - name resolution. + Root reads the system unit; an unprivileged process reads only its own user unit, never the system + one — a bare system unit pinning ``profiles/`` would otherwise alias that profile onto the + user's default unit. The user unit matters for a custom root: the ``hermes-gateway-`` naming + (#105525) left a bare user unit installed before it pinning that same root, so lifecycle commands + saw "no unit", ``gateway restart`` fell through to a foreground run, and the still-enabled legacy + unit's ``Restart=always`` looped on the instance lock (#109476). A unit pinning THIS home is this + home's service, whatever it is named. + + Linux-gated: a systemd unit is not an identity authority for launchd labels, Windows scheduled tasks, + or s6 slots, which share ``_profile_suffix()``. ``is_linux()`` is a plain ``sys.platform`` test; + ``supports_systemd_services()`` would be wrong here, since it can shell out to ``systemctl + is-system-running`` on WSL/containers and this runs on every name resolution. """ - if not is_linux() or os.geteuid() != 0: # windows-footgun: ok — behind is_linux() + if not is_linux(): return None - pinned = _hermes_home_pinned_by_unit(_SYSTEM_UNIT_DIR / f"{_SERVICE_BASE}.service") + unit_dir = _SYSTEM_UNIT_DIR if os.geteuid() == 0 else user_systemd_unit_dir() # windows-footgun: ok — behind is_linux() + pinned = _hermes_home_pinned_by_unit(unit_dir / f"{_SERVICE_BASE}.service") if not pinned: return None try: diff --git a/tests/hermes_cli/test_gateway_unit_paths.py b/tests/hermes_cli/test_gateway_unit_paths.py index 81bdc6aed5..43143081ba 100644 --- a/tests/hermes_cli/test_gateway_unit_paths.py +++ b/tests/hermes_cli/test_gateway_unit_paths.py @@ -26,3 +26,33 @@ def test_user_unit_dir_follows_the_account_home_not_a_profile_pinned_process_hom assert unit_path.parent == account_home / ".config" / "systemd" / "user" assert not unit_path.is_relative_to(process_home) + +def test_bare_user_unit_pinning_this_custom_home_is_adopted_by_lifecycle_commands(tmp_path, monkeypatch): + # A pre-#106611 install of a custom root left ``hermes-gateway.service`` (bare) pinning that root; + # the recomputed ``hermes-gateway-`` name made it invisible and ``restart`` ran foreground. + custom_root = tmp_path / "hermes" + custom_root.mkdir() + unit_dir = tmp_path / "xdg" / "systemd" / "user" + unit_dir.mkdir(parents=True) + monkeypatch.setenv("HERMES_HOME", str(custom_root)) + monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "xdg")) + monkeypatch.setattr(gateway, "is_linux", lambda: True) + monkeypatch.setattr(gateway.os, "geteuid", lambda: 1000) + monkeypatch.setattr(gateway, "supports_systemd_services", lambda: True) + legacy_unit = unit_dir / "hermes-gateway.service" + + # A bare unit that pins ANOTHER home is someone else's service: this home keeps its own name. + legacy_unit.write_text( + f'[Service]\nEnvironment="HERMES_HOME={tmp_path / "other"}"\nExecStart=/usr/bin/hermes gateway run\n', + encoding="utf-8", + ) + assert gateway.get_service_name() != "hermes-gateway" + assert not gateway._systemd_unit_installed() + + legacy_unit.write_text( + f'[Service]\nEnvironment="HERMES_HOME={custom_root}"\nRestart=always\nExecStart=/usr/bin/hermes gateway run\n', + encoding="utf-8", + ) + assert gateway.get_service_name() == "hermes-gateway" + assert gateway.get_systemd_unit_path(system=False) == legacy_unit + assert gateway._systemd_unit_installed()