From 001cd1f91dd486242bf22ce44b51336f64d00f23 Mon Sep 17 00:00:00 2001 From: Hermes Team Date: Tue, 1 Sep 2026 12:56:47 +0700 Subject: [PATCH] =?UTF-8?q?fix(installer):=20=D1=83=D1=81=D1=82=D0=B0?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D0=BA=D0=B0=20=D0=BD=D0=B0=20Linux=20=D0=BF?= =?UTF-8?q?=D0=B0=D0=B4=D0=B0=D0=BB=D0=B0=20=D0=BD=D0=B0=20=D0=B7=D0=B0?= =?UTF-8?q?=D1=89=D0=B8=D1=89=D1=91=D0=BD=D0=BD=D0=BE=D0=BC=20=D1=81=D0=B8?= =?UTF-8?q?=D1=81=D1=82=D0=B5=D0=BC=D0=BD=D0=BE=D0=BC=20Python?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Установка на сервере владельца прервалась на проверке: «No module named 'fastapi'». Причин две, и обе в установщике. Первая: запуск через sudo. Установка пользовательская — всё ложится в $HOME/.hermes и $HOME/.local/bin, root не нужен. Под sudo домашним каталогом становится /root, венв Hermes там не находится, и установщик уходит на системный python. Копия при этом ложится в /root/.hermes, где владелец её не видит. Теперь запуск через sudo распознаётся и отклоняется с объяснением; осознанный обход остаётся через HERMES_ALLOW_ROOT=1. Вторая: системный python в Ubuntu 24.04 помечен EXTERNALLY-MANAGED (PEP 668) и отклоняет pip install — и обычный, и с --user. Установщик обе неудачи проглатывал (|| true) и продолжал работу до отказа на проверке. Теперь при защищённом системном python создаётся собственное окружение $HERMES_HOME/venv, а неудача установки зависимостей прекращает установку с внятным кодом возврата. --break-system-packages не применяется: имя флага не преувеличивает. Пусковик научен той же ветке — иначе запуск уходил бы на системный python, где зависимостей нет и быть не может. Проверено исполнением на сервере под непривилегированным пользователем: окружение создано, зависимости установлены, HERMES_HUB_LINUX_VERIFY_OK, запуск через sudo отклонён с кодом 3. Co-Authored-By: Claude Opus 5 --- installer/HermesHubSetup.cs | 2 +- installer/install-linux.sh | 69 +++++++++++++++++++++++++++++++++--- launcher/HermesHub.exe | Bin 6144 -> 6144 bytes launcher/HermesHubWeb.exe | Bin 20480 -> 20992 bytes launcher/hermes-hub-web.sh | 5 +++ tests/test_installer.py | 47 ++++++++++++++++++++++++ 6 files changed, 117 insertions(+), 6 deletions(-) diff --git a/installer/HermesHubSetup.cs b/installer/HermesHubSetup.cs index c2b70f1..f4f4521 100644 --- a/installer/HermesHubSetup.cs +++ b/installer/HermesHubSetup.cs @@ -18,7 +18,7 @@ namespace HermesHubSetup // Подставляется сборщиком из фактического git-коммита. Раньше здесь // жил зашитый "8cddc9f", то есть манифест сообщал неправду о том, из // какого кода собран установщик. - public const string BuildCommit = "8daeafa"; + public const string BuildCommit = "9641957"; public const string MIN_HERMES_VERSION = "0.20.0"; public const string MAX_TESTED_HERMES = "0.20.4"; diff --git a/installer/install-linux.sh b/installer/install-linux.sh index b77c45c..3f9556d 100644 --- a/installer/install-linux.sh +++ b/installer/install-linux.sh @@ -11,6 +11,27 @@ HUB_VERSION="0.1.3" DEFAULT_HERMES_HOME="$HOME/.hermes" HERMES_HOME="${HERMES_HOME:-$DEFAULT_HERMES_HOME}" +# Установка пользовательская: всё ложится в $HOME/.hermes и $HOME/.local/bin, +# службы не ставятся, root не нужен. Под sudo домашним каталогом становится +# /root, венв Hermes там не находится, установщик сваливается на системный +# python — а он в Ubuntu 24.04 закрыт для pip (PEP 668). В итоге установка +# уходит в /root/.hermes, где владелец её не видит, и падает на проверке. +# Молча ставить не туда нельзя, поэтому отказываемся сразу и по делу. +if [ -n "${SUDO_USER:-}" ] && [ "$SUDO_USER" != "root" ] && [ -z "${HERMES_ALLOW_ROOT:-}" ]; then + echo "❌ Установщик запущен через sudo." >&2 + echo "" >&2 + echo " Hermes Hub ставится в домашний каталог пользователя, а под sudo" >&2 + echo " это /root — туда, где ни аккаунты, ни Hermes Agent не лежат." >&2 + echo "" >&2 + # $0 здесь — распакованная копия во временном каталоге, называть её + # владельцу бессмысленно: этого файла через минуту не будет. + echo " Запустите тот же установщик от своего имени, без sudo." >&2 + echo "" >&2 + echo " Если установка в /root действительно нужна, задайте" >&2 + echo " HERMES_ALLOW_ROOT=1." >&2 + exit 3 +fi + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" @@ -84,6 +105,8 @@ if [ -f "$HERMES_HOME/hermes-agent/venv/bin/python3" ]; then PYTHON_BIN="$HERMES_HOME/hermes-agent/venv/bin/python3" elif [ -f "$HERMES_HOME/hermes-agent/venv/bin/python" ]; then PYTHON_BIN="$HERMES_HOME/hermes-agent/venv/bin/python" +elif [ -x "$HERMES_HOME/venv/bin/python3" ]; then + PYTHON_BIN="$HERMES_HOME/venv/bin/python3" elif command -v python3 >/dev/null 2>&1; then PYTHON_BIN="$(command -v python3)" elif command -v python >/dev/null 2>&1; then @@ -111,12 +134,48 @@ echo "[2/6] Verifying Python dependencies..." DEPS_OK=true "$PYTHON_BIN" -c "import fastapi, uvicorn, pydantic, psutil, yaml; print('DEPS_OK')" >/dev/null 2>&1 || DEPS_OK=false +HUB_DEPS="fastapi uvicorn pydantic psutil pyyaml" + if [ "$DEPS_OK" != "true" ]; then - echo " Installing required packages (fastapi, uvicorn, pydantic, psutil, pyyaml)..." - "$PYTHON_BIN" -m pip install --no-warn-script-location -q fastapi uvicorn pydantic psutil pyyaml || { - echo "⚠️ Warning: pip install returned non-zero code. Trying with --user..." - "$PYTHON_BIN" -m pip install --user --no-warn-script-location -q fastapi uvicorn pydantic psutil pyyaml || true - } + # Системный python в Debian и Ubuntu помечен как externally-managed + # (PEP 668) и отклоняет pip install — и обычный, и с --user. Обходить это + # через --break-system-packages нельзя: имя флага не преувеличивает, так + # ломают питон всей машины. Правильный ответ — собственный venv. + NEED_VENV=false + if [ -f "/usr/lib/python$PY_VER/EXTERNALLY-MANAGED" ] || [ -f "/usr/lib/python3/EXTERNALLY-MANAGED" ]; then + case "$PYTHON_BIN" in + */venv/bin/*) : ;; + *) NEED_VENV=true ;; + esac + fi + + if [ "$NEED_VENV" = "true" ]; then + echo " Системный Python защищён от изменений (PEP 668)." + echo " Создаю отдельное окружение: $HERMES_HOME/venv" + if ! "$PYTHON_BIN" -m venv "$HERMES_HOME/venv" 2>/dev/null; then + echo "❌ Не удалось создать виртуальное окружение." >&2 + echo " Установите пакет python3-venv:" >&2 + echo " sudo apt install python3-venv" >&2 + exit 11 + fi + PYTHON_BIN="$HERMES_HOME/venv/bin/python3" + echo " Using Python: $PYTHON_BIN" + fi + + echo " Installing required packages ($HUB_DEPS)..." + # shellcheck disable=SC2086 + if ! "$PYTHON_BIN" -m pip install --no-warn-script-location -q $HUB_DEPS; then + echo "❌ Не удалось установить зависимости через $PYTHON_BIN." >&2 + echo " Установка прервана: без них хаб не запустится." >&2 + exit 12 + fi +fi + +# Проверяем результат, а не код возврата pip: установка «прошла», а модуля нет — +# именно так предыдущая сборка дошла до проверки и упала на ней. +if ! "$PYTHON_BIN" -c "import fastapi, uvicorn, pydantic, psutil, yaml" >/dev/null 2>&1; then + echo "❌ Зависимости не импортируются даже после установки ($PYTHON_BIN)." >&2 + exit 13 fi # 3. Mirror Plugin Files to ~/.hermes/plugins/antigravity-provider (with cleanup of stale files) diff --git a/launcher/HermesHub.exe b/launcher/HermesHub.exe index 1f154d049cc7a1c4622ee1e878f38d4807f1eb0c..cd5b237ed3cbfd03cb611f8f4a070ecb2f3903a6 100644 GIT binary patch delta 36 ucmV+<0Nek7Fn}z|Ns3rE4#K}eWmdub1)+bT1 zhYei#BmrztPXoA631ZYC_Ngda8$#DJzK+ld-~pkDjt2h{ANA0LP0KKcKRGl2HJ0^X zX5BOXEKX}uUM2}R1)wjvpmA*y>W(cD=QQ>(lwv$OPoN$=D@zds=b#U4*Dp_ufDnAh z*9gs1Xt!?j-Hlur>ZjQkdx(t$?9)Ibesde#ufdDwqU?d^0Hle*=@R@35Ec)V$M1y! zV%00)kOACS6)RTD1@s}=+75givL6ab@WFShI(bZ|XH8{ce!<8jOjXGz6xGlYnrX-h zGi?+!LsgZuqFI}f{peI>hRRf@23?~fEYlUbL|3VbjE>P9ySq3>a_mjYXU~ENr1hNX zM(9xnd$NOC6+3lWp^KR1-5O5GVgC}Hr6yVhnG5Lo2A`I5C+_#il4cAK~L#5?A_ s!&1e#cjx3|@MnF`W3dwn3cVV8Tx`EwKe9$ke{90ndCPO@m~>?OKl&Gc+W-In delta 449 zcmZoz!q~8YaYBdE&k0kr7@*(*0}~@RBR2zs8&H4)BDgMf;;Jydm{bM^e~>@}1H%WF ziHgFL6&d5XwzvXC6oA-mau=gEqsQcJjOvVjlkYJqDNF!r009pmW@2DvFfe9d&@p0Q zU6$nhyXra`x|!)ZyIMNBnYg() zni`sIUZJ={X|kiMxgf)uZ!foHE8BT5{B~~Em%mHJHg~x0XX4;uXAx@X6q1}A=%Fpb rz|aB>G?26Af!GQY6~&k)q)uMu(XpA&>lZ822Gh-g0q+?n8n6QZl`dfq diff --git a/launcher/hermes-hub-web.sh b/launcher/hermes-hub-web.sh index 4b424d8..a759ec2 100644 --- a/launcher/hermes-hub-web.sh +++ b/launcher/hermes-hub-web.sh @@ -16,6 +16,11 @@ if [ -f "$HERMES_HOME/hermes-agent/venv/bin/python3" ]; then PYTHON_BIN="$HERMES_HOME/hermes-agent/venv/bin/python3" elif [ -f "$HERMES_HOME/hermes-agent/venv/bin/python" ]; then PYTHON_BIN="$HERMES_HOME/hermes-agent/venv/bin/python" +elif [ -x "$HERMES_HOME/venv/bin/python3" ]; then + # Окружение, созданное установщиком, когда системный python закрыт + # правилом PEP 668. Без этой ветки запуск уходил бы на /usr/bin/python3, + # где зависимостей нет и быть не может. + PYTHON_BIN="$HERMES_HOME/venv/bin/python3" elif command -v python3 >/dev/null 2>&1; then PYTHON_BIN="$(command -v python3)" elif command -v python >/dev/null 2>&1; then diff --git a/tests/test_installer.py b/tests/test_installer.py index ff77072..d8e7d28 100644 --- a/tests/test_installer.py +++ b/tests/test_installer.py @@ -72,3 +72,50 @@ def test_silent_installer_fails_without_hermes(tmp_path): res = subprocess.run([str(SETUP_EXE), "/silent"], env=env, capture_output=True, text=True) assert res.returncode != 0, f"Expected failure for missing Hermes, got {res.returncode}" + + +# ── Установка на Linux: sudo и защищённый системный Python ── +# +# Установка на сервере владельца упала так: запуск через sudo увёл всё в +# /root/.hermes, венв Hermes там не нашёлся, установщик взял /usr/bin/python3, +# а он в Ubuntu 24.04 помечен EXTERNALLY-MANAGED и отклоняет pip. Проверка +# после установки честно упала на «No module named 'fastapi'». + +def _linux_installer_text() -> str: + root = Path(__file__).resolve().parent.parent + return (root / "installer" / "install-linux.sh").read_text(encoding="utf-8") + + +def test_linux_installer_refuses_sudo(): + text = _linux_installer_text() + assert "SUDO_USER" in text, "запуск через sudo обязан распознаваться" + assert "HERMES_ALLOW_ROOT" in text, "должен быть осознанный способ обойти отказ" + assert "exit 3" in text + + +def test_linux_installer_creates_own_venv_when_system_python_is_managed(): + text = _linux_installer_text() + assert "EXTERNALLY-MANAGED" in text, "PEP 668 должен распознаваться" + assert "-m venv" in text, "ответ на PEP 668 — собственное окружение" + # --break-system-packages ломает питон всей машины: имя флага не + # преувеличивает. Упоминание в пояснении допустимо, применение — нет, + # поэтому смотрим только на исполняемые строки. + code_lines = [ + line for line in text.splitlines() if not line.lstrip().startswith("#") + ] + assert not any("--break-system-packages" in line for line in code_lines) + + +def test_linux_installer_fails_loudly_on_missing_deps(): + """Раньше pip падал, а установка продолжалась — до отказа на проверке.""" + text = _linux_installer_text() + assert "exit 12" in text and "exit 13" in text + assert "|| true" not in text.split("Installing required packages")[1][:400] + + +def test_linux_launcher_knows_about_installer_venv(): + root = Path(__file__).resolve().parent.parent + launcher = (root / "launcher" / "hermes-hub-web.sh").read_text(encoding="utf-8") + assert '$HERMES_HOME/venv/bin/python3' in launcher, ( + "иначе запуск уйдёт на системный python, где зависимостей нет" + )