Files
knowledge-base/decisions/2026-06-18-german-hermes-agent-deploy.md
dttb 12705f148f German: веб-дашборд Hermes на german.dttb.ru (NPM #40, LE id130)
Подняли родную панель hermes dashboard на LXC 141 за NPM с basic_auth.
Грабля: флаг --insecure отключает cookie-gate (включает легаси _SESSION_TOKEN)
→ login ok, но всё внутри 401. Фикс: бинд 0.0.0.0 БЕЗ --insecure.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 17:15:32 +03:00

16 KiB
Raw Permalink Blame History

date, tags
date tags
2026-06-18
decision
ai
hermes
telegram
lxc
assistant

German — Hermes Agent как личный супер-ассистент Олега (LXC 141)

Контекст

Олег попросил поставить Hermes Agent (NousResearch, open-source, MIT, Python, model-agnostic — из ресёрча ../notes/claude/2026-06-08-145004-найди-аналог-openclaw-для-меня-нужен-аркестратор-и) на homelab. Сначала — «для тестирования самого Hermes через Telegram», затем расширил: настроить как супер-помощника с полным доступом к базе знаний. Имя — Герман (German).

Что развёрнуто

  • LXC 141 german (10.0.0.141, Debian 12, 2 vCPU / 3 GB / 12 GB на local-lvm, nesting, unprivileged, onboot=1). Создан из debian-12-standard 12.12.
  • Hermes Agent v0.16.0 — установлен официальным install.sh --non-interactive --skip-setup. Код /usr/local/lib/hermes-agent, данные /root/.hermes (uv + managed Node).
  • Telegram-бот «Герман Непомнящий» @german_dttb_bot (id 8885932329). Gateway = systemd hermes-german.service (hermes gateway run --replace, Restart=always, drain 210s, crash-guard, NoNewPrivileges/PrivateTmp).

Модель

  • Провайдер OmniRoute (OpenAI-совместимый шлюз на LXC 132): base_url http://10.0.0.179:20128/v1.
  • Активная модель: cc/claude-opus-4-8 (Opus 4.8 via Max). Fallback-цепочка (2026-06-21): cx/gpt-5.5cc/claude-sonnet-4-6; api_max_retries: 1. (Было api_max_retries: 6 + fallback cc/sonnet-4-6 первым → German отвечал по 533с/9мин: 6 ретраев с бэкоффом на каждой капнутой Max-модели. Фикс: 1 попытка + независимый cx/gpt-5.5 первым → failover за секунды. Ответ 13с после фикса.) Fallback в Hermes срабатывает на 400 «out of extra usage» (проверено по логам cc→fallback) — поэтому рабочий fallback критичен. ⚠️ Прежний fallback kr/claude-sonnet-4.5 сдох: OmniRoute потерял креды провайдера Kiro («No credentials for provider: kiro», 2026-06-19) → German падал ПОЛНОСТЬЮ (и primary капнут, и fallback мёртв). cx/gpt-5.5 выбран финальным fallback потому, что Codex — отдельный провайдер, не Max → переживает полный кап Max-квоты. Проверено: cx/gpt-5.5 работает через агентский цикл с тулами+KB.
  • КОРЕНЬ 400 «out of extra usage» (исправленное понимание): это НЕ персистентное исчерпание квоты. Олег верно заметил: «если бы лимиты — ты (Claude на Opus 4.8) тоже бы не работал». Проверка по факту 2026-06-18 ~22:30: curl cc/claude-opus-4-8 к OmniRoute с system-prompt 14B / 2КБ / 8КБ → все 200. То есть 400 в 19:11/19:18 был транзиентным — краткий кап 5-часового окна Max в момент пиковой нагрузки (Max делят this-session/German/openclaw/swarmclaw/code-server). Окно отпускает само. Если 400 участятся — включить overflow (pay-as-you-go) на claude.ai/settings/usage.
  • ⚠️ cc/claude-opus-4-8 (Max) ФЛАПАЕТ 400: You're out of extra usage: прямой curl к OmniRoute то проходит (19:16), то нет — реальные запросы Олега падали (19:11 «Привет», 19:18 «Бенелюкс», разные request_id). Причина: включённая Max-квота Opus в текущем окне исчерпана (overflow/pay-as-you-go выключен), а окно делят openclaw (cc/opus-4-7) + swarmclaw (cc/opus-4-8) + code-server через тот же OmniRoute cc/*.
  • 400 — non-retryable BadRequestError → fallback НЕ срабатывает (Hermes уводит в fallback только на rate-limit/5xx/connection). Поэтому fallback на Sonnet от Opus-400 не спасает.
  • Решение: primary = kr/claude-sonnet-4.5 (free, Kiro/AWS, не флапает, не ест Max-квоту, не конкурирует с другими ботами Олега). Проверено: запрос «Бенелюкс» через тулы вернул корректную сводку по KB.
  • Вернуть Opus, когда окно Max освободится / включён overflow: sed -i 's|kr/claude-sonnet-4.5|cc/claude-opus-4-8|' /root/.hermes/config.yaml && systemctl restart hermes-german.
  • Доп. правки сессии: отключён display.platforms.telegram.streaming (было задвоение сообщений в Telegram); fallback-цепочка протестирована (формат fallback_providers: [{provider,model,base_url}], ключ берётся из env — работает), но снята, т.к. от 400 не помогает.
  • Ключ — OPENAI_API_KEY + OPENAI_BASE_URL в /root/.hermes/.env (chmod 600).
  • auxiliary (vision/web_extract/compression/session_search) → provider: main — иначе лезли бы в OpenRouter (ключа нет) и падали.
  • ⚠️ Грабля для будущего: оба воркфлоу-ревьюера по коду утверждали, что на приватный IP-эндпоинт OPENAI_API_KEY из env «гейтится по хосту» и нужен model.api_key: ${OPENAI_API_KEY} в config.yaml, иначе 401. Эмпирически опровергнуто — CLI- и gateway-вызовы к 10.0.0.179 проходят с env-ключом без model.api_key. Если когда-нибудь начнёт давать 401 на первом вызове модели — добавить api_key: ${OPENAI_API_KEY} в секцию model: и systemctl restart hermes-german.

База знаний (KB)

  • Зеркало vault клонировано в /root/german/knowledge-base (workspace = /root/german, systemd WorkingDirectory).
  • Обновление: /root/kb-pull.sh (cron */15) делает git fetch + git reset --hard origin/main — это read-only зеркало (правки агента там затираются; для своих заметок у него /root/german/notes + native memory).
  • Безопасность кред: из .git/config workspace убран токен Gitea (был https://oleg:TOKEN@...); remote сделан tokenless, креды вынесены в /root/.git-credentials (chmod 600, вне workspace).
  • Доступ Германа к KB: тулсеты file (read_file/grep) + terminal. Семантического индекса нет — навигация через knowledge-base/CLAUDE.md → grep. Это прописано в SOUL.md.

Конфиг (ключевое, /root/.hermes/config.yaml, 600)

  • platform_toolsets.telegramявный список [web, terminal, file, memory, todo, skills, session_search, tts] (+ kanban по дефолту).
  • Почему не пресет hermes-telegram: security-ревью (читало toolsets.py:440) показало, что пресет тянет полный core-набор (browser, execute_code, computer_use), а disabled_toolsets их НЕ убирает (категории — web/browser/terminal/code_execution/image_gen). Для бота с доступом к секретам это лишние каналы исхода. Проверено hermes tools --summary: на Telegram 9/26 тулов, browser/code-exec/computer-use отсутствуют.
  • web-поиск без ключей: web.backend: ddgs (DuckDuckGo, бесплатно). Browser недоступен (требует BROWSERBASE_API_KEY) — веб через ddgs + curl в terminal.
  • SOUL.md (личность «Герман», навигация по KB, правила безопасности) — 600.
  • Прочее: display.language: ru, session_reset: idle 2880m, memory_enabled, security.redact_secrets: true + tirith_enabled: true, stt (faster-whisper base — голосовые транскрибируются).

Безопасность — модель угроз

  • Доступ только у Олега: TELEGRAM_ALLOWED_USERS=1292155421, guest_mode: false, unauthorized_dm_behavior: ignore. Проверено по коду (telegram.py/authz_mixin.py): пустой allowlist = deny-all, fail-closed. Чужие сообщения молча игнорируются.
  • terminal=local + root = Герман может выполнять любой bash на 141 и читать секреты из KB. Это намеренно (DevOps-ассистент), защита держится на allowlist. Жёсткий systemd-сэндбокс (ProtectSystem/ProtectHome) не ставил — сломал бы функции. Будущее ужесточение: отдельный непривилегированный юзер.
  • redact_secrets: true — секреты вычищаются из tool output/логов/ответов перед доставкой.

Эксплуатация

  • Статус/логи: systemctl status hermes-german; логи в файлах /root/.hermes/logs/{gateway,agent}.log (не в journal).
  • Рестарт после правок config/SOUL: systemctl restart hermes-german (дренаж до ~180с).
  • Бэкапы конфигов: /root/.hermes/config.yaml.bak.*, .env.bak.orig.
  • Эндпоинт-санити: curl -s http://10.0.0.179:20128/v1/models (должен отдавать cc/claude-opus-4-8).

Проверено

  • Установка, конфиг грузится, hermes status → Model cc/claude-opus-4-8 / Custom endpoint.
  • End-to-end CLI: вопрос по KB → grep → верный ответ (IP openclaw 10.0.0.239).
  • Прямой вызов OmniRoute cc/claude-opus-4-8 (стрим).
  • Gateway: ✓ telegram connected (polling), getMe ok, allowlist на Олега.
  • Тулсет Telegram безопасен (без browser/code-exec/computer-use).
  • Live gateway-раунд-трип: сообщение Олега «Привет» прошло Telegram→allowlist→OmniRoute→Anthropic (ключ резолвится в gateway без model.api_key — опасения ревью не подтвердились). На cc/opus упёрлись в квоту Max (400); на kr/claude-sonnet-4.5 — KB-вопрос через тулы вернул верный IP.

NetBird (добавлено 2026-06-18)

Чтобы German дотягивался до клиентских площадок в mesh (как openclaw/swarmclaw).

  • IP 100.70.99.82, german.netbird.cloud, группа Claude-Diag. Setup-key в ../projects/dttb/credentials.md (64DF527E-…, создан через PAT/API).
  • Установка только через apt-репозиторий pkgs.netbird.io — официальный curl install.sh | sh редиректит на github.com/netbirdio/.../releases/download/v0.73.0/install.sh, а GitHub releases таймаутят из RU-сети (connection timed out). Шаги: добавить ключ pkgs.netbird.io/debian/public.key в keyring + deb [...] https://pkgs.netbird.io/debian stable mainapt install netbird (v0.73.0). Работает в unprivileged-LXC через nesting=1 + kernel WireGuard (wt0, tun не нужен).
  • Enroll с --disable-dns: netbird up --setup-key <KEY> --disable-dns --hostname german. Это сознательно — на LXC 132/137 NetBird-DNS (resolv.conf через wt0) уже ломал связность; German держит свой 1.1.1.1/8.8.8.8 и ходит по IP. Magic-DNS *.netbird.cloud в контейнере НЕ резолвится (search dttb.ru хайджачит) — только по IP 100.70.x.
  • Проверено: openclaw 100.70.167.54:18789 → 200; github/telegram/OmniRoute/DNS целы; netbird autostart enabled; hermes-german не пострадал.

Веб-дашборд german.dttb.ru (добавлено 2026-06-26)

Родная веб-панель Hermes (hermes dashboard: чат + config + sessions + встроенный PTY-терминал), не путать с Open WebUI.

  • systemd hermes-dashboard.service (рядом с gateway, не вместо): hermes dashboard --host 0.0.0.0 --port 9119 --skip-build --no-open, Restart=always, бинд 0.0.0.0:9119.
  • Фронт пришлось собрать: web/dist отсутствовал. cd /usr/local/lib/hermes-agent/web && npm install && npm run build → vite кладёт в ../hermes_cli/web_dist (outDir в vite.config, НЕ web/dist), оттуда --skip-build и отдаёт. node v22 (warning EBADENGINE про node≥24 — не критично).
  • NPM #40 (10.0.0.195) german.dttb.ru10.0.0.141:9119, force-SSL+WSS+HTTP2, block-exploits. LE-серт id130 (HTTP-01, до 24.09.2026). NPM v2.14: cert-create только meta:{} (email/agree/dns_challenge = «additional properties»).
  • DNS: публичный A german.dttb.ru176.62.183.186 добавлен через Spaceweb API (editMain action:add, один вызов — циклом ломает зону). Локально wildcard *.dttb.ru10.0.0.195 уже резолвил. Проверять пропагацию только DoH (8.8.8.8/resolve), dig хайджачится локальным DNS.
  • Аутентификация — dashboard.basic_auth в /root/.hermes/config.yaml: username: oleg, password_hash (scrypt, через plugins.dashboard_auth.basic.hash_password), secret (32 байта hex), session_ttl_seconds: 43200, public_url: https://german.dttb.ru. Плейнтекст-пароль не хранится. Креды в ../projects/dttb/credentials.md. Бэкап config.yaml.bak-dashboard-20260626.
  • ⚠️ ГЛАВНАЯ ГРАБЛЯ — --insecure отключает gate. should_require_auth(host, allow_public): non-loopback + --insecureauth_required=False → активен легаси _SESSION_TOKEN-middleware (ephemeral токен, инжектится в loopback-HTML), basic-cookie-gate gated_auth_middleware становится no-op. Симптом: /auth/password-login{"ok":true} (кука минтится), но /api/auth/me + все данные→401. Провайдер/секрет/токен/куки исправны (доказано: офлайн _unsign(live_token, _resolve_secret(cfg))→payload ок; round-trip провайдера→ok). Фикс = убрать --insecure (бинд 0.0.0.0 без него: auth_required=True, провайдер basic, и 0.0.0.0 принимает любой Host — иначе явный бинд на IP даёт 400 «Invalid Host header» на NPM-домен). Hermes сам это документирует: dashboard_register.py:218 «To require login (LAN/public): hermes dashboard --host 0.0.0.0». Лог-маркер: Dashboard binding to 0.0.0.0 with OAuth auth gate enabled. Providers: basic.
  • Проверено end-to-end через NPM: https://german.dttb.ru login oleg/api/auth/me→200, /api/sessions→200; http→301; серт CN german.dttb.ru. Telegram-gateway не задет (оба сервиса active+enabled).

TODO / на будущее

  • Fallback-цепочка (cc/sonnet-4-6, kr/sonnet-4.5, cx/gpt-5.4) — формат fallback_providers с api_key_env (НЕ ${VAR} — путь резолва фолбэков не делает env-подстановку). Пока не ставил (primary надёжен).
  • Настоящий web-search через self-hosted SearXNG (SEARXNG_URL) или ключ Tavily/Firecrawl — если ddgs будет мало.
  • Рассмотреть запуск под non-root юзером для уменьшения blast radius.