Gateway install headless/non-TTY prompt handling
Cluster 42065 · hermes cli. Data as of
1 open item. Fix merged in #54163 on 28 Jun 2026. Check it covers your case.
The short answer
- Known?
- Yes. 5 related items grouped here, 1 issue already closed.
- Fix event
- A fix was merged to main in PR #54163. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
partially resolved, deciding PR #54163. That is separate from the fix event. - Still open
- 1 issue and 0 PRs.
Next step: read #54163, then check whether your report is the one still open.
Read #54163Pick up a Lab task
These issues and PRs address a regression where hermes gateway install requires interactive prompts even in non-TTY or headless environments, and add a --yes flag plus fixes to honor non-interactive context and startup flags during systemd installs.
Open issues1
- Issue#60244Feature: hermes gateway install --yes for headless provisioning (+ piped-stdin prompt quirk on Windows)
Open PRs0
Fixes on record
-
#54163 fix(gateway): headless systemd install — honor --start flags + non-TTY defaults (#42065)
files:
hermes_cli/gateway.py,hermes_cli/subcommands/gateway.py,tests/hermes_cli/test_gateway.pyShow the diff
-
hermes_cli/gateway.py@@ -3044,6 +3044,7 @@ def systemd_install( system: bool = False, run_as_user: str | None = None, enable_on_startup: bool = True, + non_interactive: bool = False, ): if system: _require_root_for_system_service("install") @@ -3057,7 +3058,7 @@ def systemd_install( print() print_legacy_unit_warning() print() - if prompt_yes_no("Remove the legacy unit(s) before installing?", True): + if non_interactive or prompt_yes_no("Remove the legacy unit(s) before installing?", True): remove_legacy_hermes_units(interactive=False) print() @@ -6268,13 +6269,31 @@ def _gateway_command_inner(args): " Or use tmux/screen for persistence: tmux new -s hermes 'hermes gateway run'" ) print() - start_now = prompt_yes_no("Start the gateway now after installing the service?", True) - start_on_login = prompt_yes_no("Start the gateway automatically on login/boot with systemd?", True) + # Honor CLI flags (--start-now / --no-start-now, --start-on-login / + # --no-start-on-login). When not provided, prompt interactively or + # fall back to True for non-TTY / headless contexts (SSH, CI, pipes). + non_interactive = not (hasattr(sys.stdin, "isatty") and sys.stdin.isatty()) + _sn = getattr(args, "start_now", None) + if _sn is not None: + start_now = _sn + elif not non_interactive: + start_now = prompt_yes_no("Start the gateway now after installing the service?", True) + else: + start_now = True + + _sol = getattr(args, "start_on_login", None) + if _sol is not None: + start_on_login = _sol + elif not non_interactive: + start_on_login = prompt_yes_no("Start the gateway automatically on login/boot with systemd?", True) + else: + start_on_login = True systemd_install( force=force, system=system, run_as_user=run_as_user, enable_on_startup=start_on_login, + non_interactive=non_interactive, ) if start_now: systemd_start(system=system) -
hermes_cli/subcommands/gateway.py@@ -169,26 +169,26 @@ def build_gateway_parser( dest="start_now", action="store_true", default=None, - help=argparse.SUPPRESS, + help="Start the gateway service immediately after installing", ) gateway_install.add_argument( "--no-start-now", dest="start_now", action="store_false", - help=argparse.SUPPRESS, + help="Do not start the gateway service after installing", ) gateway_install.add_argument( "--start-on-login", dest="start_on_login", action="store_true", default=None, - help=argparse.SUPPRESS, + help="Enable the service to start automatically on login/boot", ) gateway_install.add_argument( "--no-start-on-login", dest="start_on_login", action="store_false", - help=argparse.SUPPRESS, + help="Do not enable the service to start on login/boot", ) gateway_install.add_argument( "--elevated-handoff", -
tests/hermes_cli/test_gateway.py@@ -444,13 +444,14 @@ def test_gateway_install_in_container_with_operational_systemd_uses_systemd(monk monkeypatch.setattr(gateway, "is_wsl", lambda: False) monkeypatch.setattr(gateway, "is_macos", lambda: False) monkeypatch.setattr(gateway, "is_managed", lambda: False) + monkeypatch.setattr("sys.stdin.isatty", lambda: True) calls = [] monkeypatch.setattr(gateway, "prompt_yes_no", lambda question, default=True: calls.append(("prompt", question, default)) or True) monkeypatch.setattr( gateway, "systemd_install", - lambda force=False, system=False, run_as_user=None, enable_on_startup=True: calls.append(("install", force, system, run_as_user, enable_on_startup)), + lambda force=False, system=False, run_as_user=None, enable_on_startup=True, **kw: calls.append(("install", force, system, run_as_user, enable_on_startup)), ) monkeypatch.setattr(gateway, "systemd_start", lambda system=False: calls.append(("start", system))) @@ -766,7 +767,7 @@ def test_install_linux_gateway_from_setup_system_choice_as_root_installs(monkeyp monkeypatch.setattr( gateway, "systemd_install", - lambda force=False, system=False, run_as_user=None, enable_on_startup=True: calls.append((force, system, run_as_user, enable_on_startup)), + lambda force=False, system=False, run_as_user=None, enable_on_startup=True, **kw: calls.append((force, system, run_as_user, enable_on_startup)), ) scope, did_install = gateway.install_linux_gateway_from_setup(force=True) @@ -782,7 +783,7 @@ def test_install_linux_gateway_from_setup_passes_startup_choice(monkeypatch): monkeypatch.setattr( gateway, "systemd_install", - lambda force=False, system=False, run_as_user=None, enable_on_startup=True: calls.append((force, system, run_as_user, enable_on_startup)), + lambda force=False, system=False, run_as_user=None, enable_on_startup=True, **kw: calls.append((force, system, run_as_user, enable_on_startup)), ) scope, did_install = gateway.install_linux_gateway_from_setup(force=False, enable_on_startup=False) @@ -796,6 +797,7 @@ def test_gateway_install_can_decline_start_now_and_startup(monkeypatch): monkeypatch.setattr(gateway, "is_wsl", lambda: False) monkeypatch.setattr(gateway, "is_macos", lambda: False) monkeypatch.setattr(gateway, "is_managed", lambda: False) + monkeypatch.setattr("sys.stdin.isatty", lambda: True) answers = iter([False, False]) calls = [] @@ -803,7 +805,7 @@ def test_gateway_install_can_decline_start_now_and_startup(monkeypatch): monkeypatch.setattr( gateway, "systemd_install", - lambda force=False, system=False, run_as_user=None, enable_on_startup=True: calls.append(("install", force, system, run_as_user, enable_on_startup)), + lambda force=False, system=False, run_as_user=None, enable_on_startup=True, **kw: calls.append(("install", force, system, run_as_user, enable_on_startup)), ) monkeypatch.setattr(gateway, "systemd_start", lambda system=False: calls.append(("start", system))) @@ -817,6 +819,119 @@ def test_gateway_install_can_decline_start_now_and_startup(monkeypatch): ] +def test_gateway_install_systemd_honors_start_now_flag(monkeypatch): + """--start-now / --no-start-now should bypass the interactive prompt.""" + monkeypatch.setattr(gateway, "supports_systemd_services", lambda: True) + monkeypatch.setattr(gateway, "is_wsl", lambda: False) + monkeypatch.setattr(gateway, "is_macos", lambda: False) + monkeypatch.setattr(gateway, "is_managed", lambda: False) + + calls = [] + monkeypatch.setattr(gateway, "prompt_yes_no", lambda question, default=True: calls.append(("prompt", question))) + monkeypatch.setattr( + gateway, + "systemd_install", + lambda force=False, system=False, run_as_user=None, enable_on_startup=True, **kw: calls.append(("install", enable_on_startup)), + ) + monkeypatch.setattr(gateway, "systemd_start", lambda system=False: calls.append(("start",))) + + args = SimpleNamespace( + gateway_command="install", force=False, system=False, + run_as_user=None, start_now=True, start_on_login=False, + ) + gateway.gateway_command(args) + + assert ("prompt", "Start the gateway now after installing the service?") not in calls + assert ("start",) in calls + assert ("install", False) in calls + + +def test_gateway_install_systemd_non_tty_uses_defaults(monkeypatch): + """Non-TTY stdin (headless/CI) should use True defaults without prompting.""" + monkeypatch.setattr(gateway, "supports_systemd_services", lambda: True) + monkeypatch.setattr(gateway, "is_wsl", lambda: False) + monkeypatch.setattr(gateway, "is_macos", lambda: False) + monkeypatch.setattr(gateway, "is_managed", lambda: False) + monkeypatch.setattr("sys.stdin.isatty", lambda: False) + + calls = [] + monkeypatch.setattr(gateway, "prompt_yes_no", lambda question, default=True: calls.append(("prompt", question))) + monkeypatch.setattr( + gateway, + "systemd_install", + lambda force=False, system=False, run_as_user=None, enable_on_startup=True, **kw: calls.append(("install", enable_on_startup)), + ) + monkeypatch.setattr(gateway, "systemd_start", lambda system=False: calls.append(("start",))) + + args = SimpleNamespace(gateway_command="install", force=False, system=False, run_as_user=None) + gateway.gateway_command(args) + + # No prompts — defaults used (start_now=True, start_on_login=True) + assert all(c[0] != "prompt" for c in calls) + assert ("install", True) in calls + assert ("start",) in calls + + +def test_gateway_install_systemd_no_start_now_flag_non_tty(monkeypatch): + """--no-start-now in non-TTY should skip starting the service.""" + monkeypatch.setattr(gateway, "supports_systemd_services", lambda: True) + monkeypatch.setattr(gateway, "is_wsl", lambda: False) + monkeypatch.setattr(gateway, "is_macos", lambda: False) + monkeypatch.setattr(gateway, "is_managed", lambda: False) + monkeypatch.setattr("sys.stdin.isatty", lambda: False) + + calls = [] + monkeypatch.setattr(gateway, "prompt_yes_no", lambda question, default=True: calls.append(("prompt", question))) + monkeypatch.setattr( + gateway, + "systemd_install", + lambda force=False, system=False, run_as_user=None, enable_on_startup=True, **kw: calls.append(("install", enable_on_startup)), + ) + monkeypatch.setattr(gateway, "systemd_start", lambda system=False: calls.append(("start",))) + + args = SimpleNamespace( + gateway_command="install", force=False, system=False, + run_as_user=None, start_now=False, start_on_login=True, + ) + gateway.gateway_command(args) + + assert all(c[0] != "prompt" for c in calls) + assert ("install", True) in calls + assert ("start",) not in calls + + +def test_gateway_install_noninteractive_skips_legacy_unit_prompt(monkeypatch, tmp_path): + """In non-TTY, the legacy-unit removal prompt in systemd_install is skipped. + + Covers the second hidden prompt that --start-now/--start-on-login do not + guard. Originally contributed via PR #42124 (kyssta-exe). + """ + monkeypatch.setattr(gateway, "has_legacy_hermes_units", lambda: True) + + calls = [] + monkeypatch.setattr( + gateway, + "prompt_yes_no", + lambda question, default=True: calls.append(("prompt", question)) or True, + ) + monkeypatch.setattr(gateway, "remove_legacy_hermes_units", lambda interactive=False: calls.append(("remove_legacy",))) + monkeypatch.setattr(gateway, "print_legacy_unit_warning", lambda: None) + + fake_path = tmp_path / "hermes-gateway.service" + monkeypatch.setattr(gateway, "get_systemd_unit_path", lambda system=False: fake_path) + monkeypatch.setattr(gateway, "generate_systemd_unit", lambda system=False, run_as_user=None: "[Service]") + monkeypatch.setattr(gateway, "_run_systemctl", lambda *a, **kw: None) + monkeypatch.setattr(gateway, "_ensure_linger_enabled", lambda: None) + monkeypatch.setattr(gateway, "print_systemd_scope_conflict_warning", lambda: None) + monkeypatch.setattr(gateway, "_service_scope_label", lambda system=False: "user") + + gateway.systemd_install(non_interactive=True) + + # Legacy units removed without prompting. + assert ("remove_legacy",) in calls + assert all(c[0] != "prompt" for c in calls) + + def test_find_gateway_pids_falls_back_to_pid_file_when_process_scan_fails(monkeypatch): monkeypatch.setattr(gateway, "_get_service_pids", lambda: set()) monkeypatch.setattr(gateway, "is_windows", lambda: False)
-
2 PRs closed without merging
From the close comments: merged via another change 2
Closed issues · 1
- Closed#42065`hermes gateway install` is non-bypassable interactive on systemd — breaks headless installs (regression in v2026.5.28)
How this cluster was grouped
-
Link evidence (
partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text. - Members outside that structural core may belong to a different underlying problem.
-
Pairwise link density
0.34: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the LabAt a glance
- Subsystem
- hermes cli
- Merged PRs
- #54163
- Closed issues
- 1
- Attempts
- 2 PRs closed without merging
- Fix link checked
- 28 Jun 2026