fix(install): probe /dev/tty by opening it, not bare existence (#16746)

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/tty) 2>/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) <noreply@anthropic.com>
This commit is contained in:
briandevans 2026-04-27 18:12:41 -07:00 committed by Teknium
parent b2339c87e4
commit 20c9340c34
2 changed files with 47 additions and 1 deletions

View File

@ -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/tty) 2>/dev/null; then
log_info "Setup wizard skipped (no terminal available). Run 'hermes setup' after install."
return 0
fi

View File

@ -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/tty) 2>/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 "(: </dev/tty)" in body, (
"run_setup_wizard must probe /dev/tty by actually opening it before "
"running the wizard. See #16746."
)