Подняли родную панель 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>
82 lines
16 KiB
Markdown
82 lines
16 KiB
Markdown
---
|
||
date: 2026-06-18
|
||
tags: [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.5` → `cc/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 main` → `apt 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.ru` → `10.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.ru`→`176.62.183.186` добавлен через Spaceweb API (`editMain` `action:add`, один вызов — циклом ломает зону). Локально wildcard `*.dttb.ru`→`10.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 + `--insecure` → `auth_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.
|