From 20c9340c34454e1bd5ca9268fa6ac8251c980ed7 Mon Sep 17 00:00:00 2001 From: briandevans <252620095+briandevans@users.noreply.github.com> Date: Mon, 27 Apr 2026 18:12:41 -0700 Subject: [PATCH] fix(install): probe /dev/tty by opening it, not bare existence (#16746) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In Docker builds the `/dev/tty` device node is present in the mount namespace, so `[ -e /dev/tty ]` returns true — but opening it fails with `ENXIO: No such device or address`. Under the old gate the "no terminal available" skip never triggered, the setup wizard ran, and the build aborted a few lines later when bash tried `< /dev/tty`: /tmp/install.sh: line 1347: /dev/tty: No such device or address Replace the existence check with `(: /dev/null`, which actually attempts to open /dev/tty in a subshell. The probe succeeds when piped from `curl | bash` on a real terminal (the wizard's intended use case) and fails cleanly in Docker build / CI contexts so the skip kicks in before the redirect can crash. Add a regression test that statically asserts run_setup_wizard does not gate on the bare existence check and that the open-based probe is in place. Fixes #16746. Co-Authored-By: Claude Opus 4.7 (1M context) --- scripts/install.sh | 7 +++- .../test_install_sh_setup_wizard_tty_probe.py | 41 +++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 tests/test_install_sh_setup_wizard_tty_probe.py diff --git a/scripts/install.sh b/scripts/install.sh index 8e8b4d9a..53d8e081 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -1330,7 +1330,12 @@ run_setup_wizard() { # The setup wizard reads from /dev/tty, so it works even when the # install script itself is piped (curl | bash). Only skip if no # terminal is available at all (e.g. Docker build, CI). - if ! [ -e /dev/tty ]; then + # + # Probe by actually opening /dev/tty: a bare existence test passes + # in Docker builds where the device node is in the mount namespace + # but opening fails with ENXIO, so the wizard would proceed and + # then crash on `< /dev/tty` below. + if ! (: /dev/null; then log_info "Setup wizard skipped (no terminal available). Run 'hermes setup' after install." return 0 fi diff --git a/tests/test_install_sh_setup_wizard_tty_probe.py b/tests/test_install_sh_setup_wizard_tty_probe.py new file mode 100644 index 00000000..dbe70538 --- /dev/null +++ b/tests/test_install_sh_setup_wizard_tty_probe.py @@ -0,0 +1,41 @@ +"""Regression for #16746: setup-wizard tty gate must actually open /dev/tty. + +In a Docker build, ``/dev/tty`` exists as a device node (so ``[ -e /dev/tty ]`` +returns true) but opening it fails with ``ENXIO: No such device or address``. +Under the old gate the wizard proceeded past the "no terminal available" skip +and then crashed on the ``< /dev/tty`` redirect a few lines later, aborting +the entire image build. The fix replaces the bare existence check with an +open-based probe so the skip kicks in correctly. +""" + +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +INSTALL_SH = REPO_ROOT / "scripts" / "install.sh" + + +def _extract_run_setup_wizard() -> str: + """Return the body of run_setup_wizard() as a single string.""" + text = INSTALL_SH.read_text() + start = text.index("run_setup_wizard()") + # The next top-level function follows immediately; use it as the end marker. + end = text.index("\nmaybe_start_gateway()", start) + return text[start:end] + + +def test_run_setup_wizard_does_not_use_bare_existence_check() -> None: + body = _extract_run_setup_wizard() + assert "[ -e /dev/tty ]" not in body, ( + "run_setup_wizard guards on `[ -e /dev/tty ]`, which is true in Docker " + "builds where the device node exists but cannot be opened (ENXIO). " + "Use an open-based probe such as `(: /dev/null` so the " + "skip kicks in before the wizard tries to read from /dev/tty. See #16746." + ) + + +def test_run_setup_wizard_uses_open_based_tty_probe() -> None: + body = _extract_run_setup_wizard() + assert "(: