hermes-hub/launcher/hermes-hub-web.sh
Hermes Team 001cd1f91d 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>
2026-09-01 12:56:47 +07:00

182 lines
7.6 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# ==============================================================================
# Hermes Hub — Linux Web Application Launcher
# Starts the background web server if not already active, waits for /api/health,
# and opens the UI in application window mode (--app=URL) or handles headless mode.
# ==============================================================================
set -e
DEFAULT_HERMES_HOME="$HOME/.hermes"
HERMES_HOME="${HERMES_HOME:-$DEFAULT_HERMES_HOME}"
# 1. Discover Python Runtime
PYTHON_BIN=""
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
PYTHON_BIN="$(command -v python)"
else
echo "❌ Error: Python 3 not found on system." >&2
exit 1
fi
# 2. Configure Environment and Read Settings (Host/Port)
export PYTHONPATH="$HERMES_HOME/plugins/antigravity-provider/src:$HERMES_HOME/hermes-agent:$PYTHONPATH"
HOST="127.0.0.1"
PORT=5800
SETTINGS_FILE="$HERMES_HOME/hub_settings.json"
if [ -f "$SETTINGS_FILE" ]; then
PARSED_PORT="$("$PYTHON_BIN" -c "import json; print(json.load(open('$SETTINGS_FILE')).get('web_api_port', 5800))" 2>/dev/null || echo 5800)"
if [ -n "$PARSED_PORT" ] && [ "$PARSED_PORT" -gt 0 ] 2>/dev/null; then
PORT="$PARSED_PORT"
fi
PARSED_HOST="$("$PYTHON_BIN" -c "import json; h = json.load(open('$SETTINGS_FILE')).get('web_api_host', '127.0.0.1'); print('127.0.0.1' if h in ('0.0.0.0', '') else h)" 2>/dev/null || echo '127.0.0.1')"
if [ -n "$PARSED_HOST" ]; then
HOST="$PARSED_HOST"
fi
fi
TARGET_URL="http://$HOST:$PORT/"
HEALTH_URL="http://$HOST:$PORT/api/health"
# Function to check server health
check_health() {
# sys импортируется здесь, а не внутри except: раньше на УСПЕШНОМ ответе
# возникал NameError, его ловил тот же except, и проверка всегда сообщала
# об отказе — сервер работал, а лаунчер писал «failed to respond».
"$PYTHON_BIN" -c "
import json
import sys
import urllib.request
try:
with urllib.request.urlopen('$HEALTH_URL', timeout=0.8) as resp:
data = json.loads(resp.read().decode('utf-8'))
sys.exit(0 if data.get('ok') is True else 1)
except Exception:
sys.exit(1)
" >/dev/null 2>&1
}
# 3. Start Web Server if not already active
SERVER_STARTED_BY_US=false
if ! check_health; then
echo "Starting Hermes Hub Web Server in background..."
LOG_FILE="$HERMES_HOME/hermes_web_server.log"
nohup "$PYTHON_BIN" -m antigravity_provider.router.web > "$LOG_FILE" 2>&1 &
SERVER_PID=$!
SERVER_STARTED_BY_US=true
# Wait for server readiness (polling /api/health)
READY=false
for i in $(seq 1 75); do
if check_health; then
READY=true
break
fi
if ! kill -0 "$SERVER_PID" 2>/dev/null; then
echo "❌ Error: Web server process terminated unexpectedly. Check $LOG_FILE" >&2
exit 1
fi
sleep 0.2
done
if [ "$READY" != "true" ]; then
echo "❌ Error: Web server failed to respond at $HEALTH_URL within 15 seconds." >&2
exit 1
fi
fi
# 4. Headless Server Check (SSH / No Graphical Display)
#
# Текст здесь раньше был зашит: он всегда советовал пробрасывать порт и
# показывал 127.0.0.1, даже когда хаб уже привязан к сети и доступен напрямую.
# Инструкция, не совпадающая с настройками, хуже отсутствующей — владелец
# поднимал туннель к серверу, который и так был виден.
if [ -z "$DISPLAY" ] && [ -z "$WAYLAND_DISPLAY" ]; then
SETTINGS_FILE="$HERMES_HOME/hub_settings.json"
BIND_HOST="127.0.0.1"
if [ -f "$SETTINGS_FILE" ]; then
BIND_HOST="$("$PYTHON_BIN" - "$SETTINGS_FILE" <<'PYEOF' 2>/dev/null || echo "127.0.0.1"
import json, sys
try:
print(json.load(open(sys.argv[1], encoding="utf-8")).get("web_api_host", "127.0.0.1"))
except Exception:
print("127.0.0.1")
PYEOF
)"
fi
echo "======================================================================"
echo " Hermes Hub Web Server is running (no graphical display)"
echo ""
if [ "$BIND_HOST" = "127.0.0.1" ] || [ -z "$BIND_HOST" ]; then
echo " Listening on 127.0.0.1:$PORT — reachable from this machine only."
echo ""
echo " To open it from another computer, forward the port:"
echo " ssh -L $PORT:127.0.0.1:$PORT <user>@<this-host>"
echo " then open http://127.0.0.1:$PORT/ there."
echo ""
echo " To expose it on the local network instead:"
echo " python3 $HERMES_HOME/scripts/enable_lan_access.py"
else
LAN_IP="$(hostname -I 2>/dev/null | awk '{print $1}')"
[ -n "$LAN_IP" ] || LAN_IP="<address of this host>"
echo " Listening on $BIND_HOST:$PORT — reachable over the network."
echo ""
echo " Open from another computer:"
echo " http://$LAN_IP:$PORT/"
echo ""
echo " A token is required: paste it in Settings on first open."
echo " No port forwarding needed."
fi
echo "======================================================================"
exit 0
fi
# 5. Graphical Environment: Search for Chromium-based browser in priority order
CHROMIUM_BIN=""
for b in google-chrome google-chrome-stable chromium chromium-browser microsoft-edge microsoft-edge-stable brave-browser; do
if command -v "$b" >/dev/null 2>&1; then
CHROMIUM_BIN="$(command -v "$b")"
break
fi
done
if [ -n "$CHROMIUM_BIN" ]; then
# Launch in Application Window mode without address bar, tabs, or menus
# Отдельный профиль: иначе запущенный браузер передаёт окно уже
# работающему экземпляру и сразу завершается — ожидание его закрытия
# срабатывает мгновенно. На Windows это приводило к тому, что сервер
# убивали, пока страница ещё грузилась.
BROWSER_PROFILE="$HERMES_HOME/web_browser_profile"
mkdir -p "$BROWSER_PROFILE"
"$CHROMIUM_BIN" --app="$TARGET_URL" --window-size=1400,900 --user-data-dir="$BROWSER_PROFILE" --no-first-run --no-default-browser-check "$@"
else
# Fallback to standard default browser
echo "Запуск в обычном браузере: режим приложения (без адресной строки) требует Google Chrome, Chromium или Microsoft Edge."
if command -v xdg-open >/dev/null 2>&1; then
xdg-open "$TARGET_URL"
elif command -v sensible-browser >/dev/null 2>&1; then
sensible-browser "$TARGET_URL"
elif command -v firefox >/dev/null 2>&1; then
firefox "$TARGET_URL"
else
echo "Please open $TARGET_URL in your web browser."
fi
fi
exit 0