fix(installer): установка на Linux падала на защищённом системном Python

Установка на сервере владельца прервалась на проверке: «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 <noreply@anthropic.com>
This commit is contained in:
Hermes Team 2026-09-01 12:56:47 +07:00
parent 9641957d5b
commit 001cd1f91d
6 changed files with 117 additions and 6 deletions

View file

@ -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";

View file

@ -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)

Binary file not shown.

Binary file not shown.

View file

@ -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

View file

@ -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, где зависимостей нет"
)