fix(gateway): refuse to write service definitions with a temp-dir HERMES_HOME - #44267
Conversation
…MES_HOME A test/E2E harness that exports HERMES_HOME=/tmp/... and touches any gateway service write path (install, start self-heal, restart's refresh_systemd_unit_if_needed) bakes the throwaway home into the production systemd unit / launchd plist. The gateway then restarts 'healthy' but pointed at an empty temp home — no platforms enabled, deaf to every message (live incident 2026-06-11: /tmp/hermes-e2e-41264 poisoned the unit during a PR-review E2E probe; the post-update restart produced a 7-hour zombie gateway). The existing safety belt only sniffed pytest-shaped markers (/pytest-of-, /hermes_test). Add a structural guard: _temp_home_in_service_definition() extracts HERMES_HOME from the generated systemd unit or launchd plist and refuses the write (with actionable guidance) when it resolves under tempfile.gettempdir(), /tmp, /var/tmp, or the macOS /private variants. Wired into all five write sites: systemd refresh + install, launchd refresh + install + start self-heal.
🔎 Lint report:
|
CI runs hermetic with HERMES_HOME under a tmp dir, so the real generate_systemd_unit() output now (correctly) trips the new temp-home write guard in three install tests. Patch the generator with synthetic non-temp content — same pattern the existing pytest-marker guard tests use.
Code Review — VerificationReviewed the diff (4 files, +231/-6). This is a well-scoped defense-in-depth guard that prevents test/E2E environments from poisoning production service definitions with a temp-directory HERMES_HOME. What I verified:
Clean implementation. No findings. |
…nt home (#f456f302d, NousResearch#44267) A test/E2E harness that exports HERMES_HOME=/tmp/... and touches any gateway service write path (install, start self-heal, restart's refresh_systemd_unit_if_needed) bakes the throwaway home into the production systemd unit / launchd plist. The gateway then restarts 'healthy' but pointed at an empty temp home — no platforms enabled, deaf to every message (upstream live incident 2026-06-11: a /tmp E2E home poisoned the unit during a PR-review probe; the post-update restart produced a 7-hour zombie gateway). The existing safety belt only sniffed pytest-shaped markers (/pytest-of-, /hermes_test). Add a structural guard: _temp_home_in_service_definition() extracts the agent home (HERMES_HOME plus our SUPERFORECASTING_AGENT_HOME/FORECAST_HOME aliases) from the generated systemd unit or launchd plist and refuses the write (with actionable guidance) when it resolves under tempfile.gettempdir(), /tmp, /var/tmp, or the macOS /private variants. Wired into all five write sites: systemd refresh + install, launchd refresh + install + start self-heal. Install tests patch the unit/plist generator with synthetic non-temp content (CI runs hermetic with the agent home under a tmp dir, which the guard now correctly refuses). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…MES_HOME (#44267) * fix(gateway): refuse to write service definitions with a temp-dir HERMES_HOME A test/E2E harness that exports HERMES_HOME=/tmp/... and touches any gateway service write path (install, start self-heal, restart's refresh_systemd_unit_if_needed) bakes the throwaway home into the production systemd unit / launchd plist. The gateway then restarts 'healthy' but pointed at an empty temp home — no platforms enabled, deaf to every message (live incident 2026-06-11: /tmp/hermes-e2e-41264 poisoned the unit during a PR-review E2E probe; the post-update restart produced a 7-hour zombie gateway). The existing safety belt only sniffed pytest-shaped markers (/pytest-of-, /hermes_test). Add a structural guard: _temp_home_in_service_definition() extracts HERMES_HOME from the generated systemd unit or launchd plist and refuses the write (with actionable guidance) when it resolves under tempfile.gettempdir(), /tmp, /var/tmp, or the macOS /private variants. Wired into all five write sites: systemd refresh + install, launchd refresh + install + start self-heal. * test: patch unit generator in install tests tripped by temp-home guard CI runs hermetic with HERMES_HOME under a tmp dir, so the real generate_systemd_unit() output now (correctly) trips the new temp-home write guard in three install tests. Patch the generator with synthetic non-temp content — same pattern the existing pytest-marker guard tests use.
…MES_HOME (NousResearch#44267) * fix(gateway): refuse to write service definitions with a temp-dir HERMES_HOME A test/E2E harness that exports HERMES_HOME=/tmp/... and touches any gateway service write path (install, start self-heal, restart's refresh_systemd_unit_if_needed) bakes the throwaway home into the production systemd unit / launchd plist. The gateway then restarts 'healthy' but pointed at an empty temp home — no platforms enabled, deaf to every message (live incident 2026-06-11: /tmp/hermes-e2e-41264 poisoned the unit during a PR-review E2E probe; the post-update restart produced a 7-hour zombie gateway). The existing safety belt only sniffed pytest-shaped markers (/pytest-of-, /hermes_test). Add a structural guard: _temp_home_in_service_definition() extracts HERMES_HOME from the generated systemd unit or launchd plist and refuses the write (with actionable guidance) when it resolves under tempfile.gettempdir(), /tmp, /var/tmp, or the macOS /private variants. Wired into all five write sites: systemd refresh + install, launchd refresh + install + start self-heal. * test: patch unit generator in install tests tripped by temp-home guard CI runs hermetic with HERMES_HOME under a tmp dir, so the real generate_systemd_unit() output now (correctly) trips the new temp-home write guard in three install tests. Patch the generator with synthetic non-temp content — same pattern the existing pytest-marker guard tests use.
…MES_HOME (NousResearch#44267) * fix(gateway): refuse to write service definitions with a temp-dir HERMES_HOME A test/E2E harness that exports HERMES_HOME=/tmp/... and touches any gateway service write path (install, start self-heal, restart's refresh_systemd_unit_if_needed) bakes the throwaway home into the production systemd unit / launchd plist. The gateway then restarts 'healthy' but pointed at an empty temp home — no platforms enabled, deaf to every message (live incident 2026-06-11: /tmp/hermes-e2e-41264 poisoned the unit during a PR-review E2E probe; the post-update restart produced a 7-hour zombie gateway). The existing safety belt only sniffed pytest-shaped markers (/pytest-of-, /hermes_test). Add a structural guard: _temp_home_in_service_definition() extracts HERMES_HOME from the generated systemd unit or launchd plist and refuses the write (with actionable guidance) when it resolves under tempfile.gettempdir(), /tmp, /var/tmp, or the macOS /private variants. Wired into all five write sites: systemd refresh + install, launchd refresh + install + start self-heal. * test: patch unit generator in install tests tripped by temp-home guard CI runs hermetic with HERMES_HOME under a tmp dir, so the real generate_systemd_unit() output now (correctly) trips the new temp-home write guard in three install tests. Patch the generator with synthetic non-temp content — same pattern the existing pytest-marker guard tests use.
…MES_HOME (NousResearch#44267) * fix(gateway): refuse to write service definitions with a temp-dir HERMES_HOME A test/E2E harness that exports HERMES_HOME=/tmp/... and touches any gateway service write path (install, start self-heal, restart's refresh_systemd_unit_if_needed) bakes the throwaway home into the production systemd unit / launchd plist. The gateway then restarts 'healthy' but pointed at an empty temp home — no platforms enabled, deaf to every message (live incident 2026-06-11: /tmp/hermes-e2e-41264 poisoned the unit during a PR-review E2E probe; the post-update restart produced a 7-hour zombie gateway). The existing safety belt only sniffed pytest-shaped markers (/pytest-of-, /hermes_test). Add a structural guard: _temp_home_in_service_definition() extracts HERMES_HOME from the generated systemd unit or launchd plist and refuses the write (with actionable guidance) when it resolves under tempfile.gettempdir(), /tmp, /var/tmp, or the macOS /private variants. Wired into all five write sites: systemd refresh + install, launchd refresh + install + start self-heal. * test: patch unit generator in install tests tripped by temp-home guard CI runs hermetic with HERMES_HOME under a tmp dir, so the real generate_systemd_unit() output now (correctly) trips the new temp-home write guard in three install tests. Patch the generator with synthetic non-temp content — same pattern the existing pytest-marker guard tests use.
…MES_HOME (NousResearch#44267) * fix(gateway): refuse to write service definitions with a temp-dir HERMES_HOME A test/E2E harness that exports HERMES_HOME=/tmp/... and touches any gateway service write path (install, start self-heal, restart's refresh_systemd_unit_if_needed) bakes the throwaway home into the production systemd unit / launchd plist. The gateway then restarts 'healthy' but pointed at an empty temp home — no platforms enabled, deaf to every message (live incident 2026-06-11: /tmp/hermes-e2e-41264 poisoned the unit during a PR-review E2E probe; the post-update restart produced a 7-hour zombie gateway). The existing safety belt only sniffed pytest-shaped markers (/pytest-of-, /hermes_test). Add a structural guard: _temp_home_in_service_definition() extracts HERMES_HOME from the generated systemd unit or launchd plist and refuses the write (with actionable guidance) when it resolves under tempfile.gettempdir(), /tmp, /var/tmp, or the macOS /private variants. Wired into all five write sites: systemd refresh + install, launchd refresh + install + start self-heal. * test: patch unit generator in install tests tripped by temp-home guard CI runs hermetic with HERMES_HOME under a tmp dir, so the real generate_systemd_unit() output now (correctly) trips the new temp-home write guard in three install tests. Patch the generator with synthetic non-temp content — same pattern the existing pytest-marker guard tests use.
…MES_HOME (NousResearch#44267) * fix(gateway): refuse to write service definitions with a temp-dir HERMES_HOME A test/E2E harness that exports HERMES_HOME=/tmp/... and touches any gateway service write path (install, start self-heal, restart's refresh_systemd_unit_if_needed) bakes the throwaway home into the production systemd unit / launchd plist. The gateway then restarts 'healthy' but pointed at an empty temp home — no platforms enabled, deaf to every message (live incident 2026-06-11: /tmp/hermes-e2e-41264 poisoned the unit during a PR-review E2E probe; the post-update restart produced a 7-hour zombie gateway). The existing safety belt only sniffed pytest-shaped markers (/pytest-of-, /hermes_test). Add a structural guard: _temp_home_in_service_definition() extracts HERMES_HOME from the generated systemd unit or launchd plist and refuses the write (with actionable guidance) when it resolves under tempfile.gettempdir(), /tmp, /var/tmp, or the macOS /private variants. Wired into all five write sites: systemd refresh + install, launchd refresh + install + start self-heal. * test: patch unit generator in install tests tripped by temp-home guard CI runs hermetic with HERMES_HOME under a tmp dir, so the real generate_systemd_unit() output now (correctly) trips the new temp-home write guard in three install tests. Patch the generator with synthetic non-temp content — same pattern the existing pytest-marker guard tests use.
…MES_HOME (NousResearch#44267) * fix(gateway): refuse to write service definitions with a temp-dir HERMES_HOME A test/E2E harness that exports HERMES_HOME=/tmp/... and touches any gateway service write path (install, start self-heal, restart's refresh_systemd_unit_if_needed) bakes the throwaway home into the production systemd unit / launchd plist. The gateway then restarts 'healthy' but pointed at an empty temp home — no platforms enabled, deaf to every message (live incident 2026-06-11: /tmp/hermes-e2e-41264 poisoned the unit during a PR-review E2E probe; the post-update restart produced a 7-hour zombie gateway). The existing safety belt only sniffed pytest-shaped markers (/pytest-of-, /hermes_test). Add a structural guard: _temp_home_in_service_definition() extracts HERMES_HOME from the generated systemd unit or launchd plist and refuses the write (with actionable guidance) when it resolves under tempfile.gettempdir(), /tmp, /var/tmp, or the macOS /private variants. Wired into all five write sites: systemd refresh + install, launchd refresh + install + start self-heal. * test: patch unit generator in install tests tripped by temp-home guard CI runs hermetic with HERMES_HOME under a tmp dir, so the real generate_systemd_unit() output now (correctly) trips the new temp-home write guard in three install tests. Patch the generator with synthetic non-temp content — same pattern the existing pytest-marker guard tests use.
…MES_HOME (NousResearch#44267) * fix(gateway): refuse to write service definitions with a temp-dir HERMES_HOME A test/E2E harness that exports HERMES_HOME=/tmp/... and touches any gateway service write path (install, start self-heal, restart's refresh_systemd_unit_if_needed) bakes the throwaway home into the production systemd unit / launchd plist. The gateway then restarts 'healthy' but pointed at an empty temp home — no platforms enabled, deaf to every message (live incident 2026-06-11: /tmp/hermes-e2e-41264 poisoned the unit during a PR-review E2E probe; the post-update restart produced a 7-hour zombie gateway). The existing safety belt only sniffed pytest-shaped markers (/pytest-of-, /hermes_test). Add a structural guard: _temp_home_in_service_definition() extracts HERMES_HOME from the generated systemd unit or launchd plist and refuses the write (with actionable guidance) when it resolves under tempfile.gettempdir(), /tmp, /var/tmp, or the macOS /private variants. Wired into all five write sites: systemd refresh + install, launchd refresh + install + start self-heal. * test: patch unit generator in install tests tripped by temp-home guard CI runs hermetic with HERMES_HOME under a tmp dir, so the real generate_systemd_unit() output now (correctly) trips the new temp-home write guard in three install tests. Patch the generator with synthetic non-temp content — same pattern the existing pytest-marker guard tests use.
Summary
Gateway service-definition writes (systemd unit / launchd plist) now refuse to bake a temp-dir
HERMES_HOMEinto the installed service, closing the path where a test/E2E shell withHERMES_HOME=/tmp/...exported silently poisons the production gateway.Root cause (live incident 2026-06-11): an E2E probe ran
hermes gateway restartwithHERMES_HOME=/tmp/hermes-e2e-41264exported. The restart path'srefresh_systemd_unit_if_needed()regenerated the unit from the current env and wroteEnvironment="HERMES_HOME=/tmp/hermes-e2e-41264"+ matchingWorkingDirectoryinto~/.config/systemd/user/hermes-gateway.service. The next planned restart (post-update) brought the gateway back "active (running)" but pointed at the empty temp home — "No messaging platforms enabled", deaf to every platform for 7+ hours, self-perpetuating because the poisoned gateway's own on-boot unit refresh re-resolved the same bad env. The existing safety belt only sniffed pytest-shaped markers (/pytest-of-,/hermes_test), which manual E2E homes don't carry.Changes
hermes_cli/gateway.py: new_temp_home_in_service_definition()— extractsHERMES_HOMEfrom a generated systemd unit (Environment=line) or launchd plist (<key>/<string>pair) and reports it when the path resolves undertempfile.gettempdir(),/tmp,/var/tmp, or the macOS/privatevariants (component-wise prefix match, no/tmpfs-datafalse positives).hermes_cli/gateway.py:_refuse_temp_home_service_write()wired into all five write sites — systemd refresh + install, launchd refresh + install + start self-heal — refusing the write with actionable guidance ("unset HERMES_HOME / run from a clean shell").tests/hermes_cli/test_gateway_service.py: 8 detector tests + a refresh-refusal test mirroring the incident unit; 2 existing launchd tests updated to patchgenerate_launchd_plistwith non-temp synthetic content (their pytest-tmp HERMES_HOME now trips the guard by design).Validation
tests/hermes_cli/test_gateway_service.pyHERMES_HOME=/tmp/hermes-e2e-guardtest)refresh_systemd_unit_if_needed()refuses, unit file untouched/home/teknium/.hermesunit passes, no false positiveInfographic