# Hermes на Eagle (Mac M4 Max) — Настройка и подводные камни Hermes работает нативно (не в Docker) на Mac через `hermes gateway`. Транспорт — Zulip (запущен в Docker). Провайдер модели — openclaw-claude-proxy (см. ниже). ## Компоненты | Компонент | Расположение | Запуск | |-----------|-------------|--------| | Hermes config | `~/.hermes/config.yaml` | Eagle Dashboard (supervisor) — `POST /api/services/hermes/start` | | claude-proxy (Claude proxy) | `/opt/homebrew/bin/claude-proxy` | launchd `ai.claude-proxy` | | Zulip stack | `~/Developer/zulip/docker-compose.yml` | `docker compose up -d` | | Obsidian MCP | mcpvault | встроен в Hermes toolset | --- ## Claude CLI прокси — обход rate limit Claude API > **Актуальный прокси (июнь 2026):** Python `claude-code-openai-wrapper` на > порту 8090. Детальная документация: [[claude-python-cli-proxy]]. ### Проблема `provider: claude-code` в Hermes использует OAuth-токен напрямую через API Anthropic — и упирается в rate limit подписки. Лимиты сбрасываются раз в час. API-ключа нет (политика организации). ### Почему не cmappy cmappy (`claude-max-proxy-py`) молча выбрасывает поле `tools` из запроса — передаёт только текст в `claude --print`. Результат: Hermes не может использовать **ни один инструмент** (скиллы, MCP, терминал). Только голый чат. ### Решение: openclaw-claude-proxy [mehdic/claude-proxy](https://github.com/mehdic/claude-proxy) (npm: `openclaw-claude-proxy`) — Node.js сервер, запускает `claude --print` как subprocess и предоставляет OpenAI-совместимый `/v1/chat/completions` на порту 3456. **Поддерживает tool_use** — инжектирует схемы инструментов в системный промпт, парсит JSON tool_call из ответа, возвращает стандартный OpenAI `tool_calls`. Caller (Hermes) сам выполняет инструменты. **Важно:** `CLAUDE_PROXY_TOOLS_TRANSLATION=1` НЕ включать — этот режим выполняет MCP инструменты внутри CLI и Hermes ничего не получает. ### Установка ```bash npm install -g openclaw-claude-proxy ``` ### Wrapper-скрипт (обязателен для launchd) `~/.local/bin/claude-proxy-start.sh`: ```bash #!/bin/zsh # launchd не наследует среду login-сессии — токен нужно загружать явно set -a source /Users/admin/.hermes/.env 2>/dev/null set +a export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:$PATH" exec /opt/homebrew/bin/claude-proxy 3456 ``` **Pitfall:** без явного `source ~/.hermes/.env` claude-proxy не видит `CLAUDE_CODE_OAUTH_TOKEN` и прогревочные процессы падают с "Not logged in". **Pitfall:** без явного PATH Claude CLI не найден (`/opt/homebrew/bin/claude` не в launchd PATH). ### launchd сервис `~/Library/LaunchAgents/ai.claude-proxy.plist`: ```xml Labelai.claude-proxy ProgramArguments /Users/admin/.local/bin/claude-proxy-start.sh EnvironmentVariables HOME/Users/admin RunAtLoad KeepAlive StandardOutPath /Users/admin/.hermes/logs/claude-proxy.log StandardErrorPath /Users/admin/.hermes/logs/claude-proxy.log ``` ```bash launchctl load ~/Library/LaunchAgents/ai.claude-proxy.plist ``` **Pitfall при перезагрузке:** если старый процесс ещё держит порт 3456: ```bash lsof -ti :3456 | xargs kill -9 launchctl unload ~/Library/LaunchAgents/ai.claude-proxy.plist launchctl load ~/Library/LaunchAgents/ai.claude-proxy.plist ``` ### Конфигурация Hermes `~/.hermes/config.yaml` (секция model): ```yaml model: default: claude-sonnet-4-6 # provider: claude-code # отключён — упирается в rate limit OAuth API provider: custom base_url: 'http://localhost:3456/v1' ``` **Pitfall:** `provider: openai` не существует в Hermes — нужно `custom`. **Pitfall:** `base_url` должен включать `/v1` (Hermes дописывает `/chat/completions`). Без `/v1` → 404. --- ## Zulip Docker — подводные камни ### RabbitMQ: пользователи сбрасываются после перезапуска **Симптом:** Zulip отдаёт 500 на `/api/v1/register`. В логах RabbitMQ — паника Khepri (Raft WAL). Пользователи в RabbitMQ исчезают. **Причина:** RabbitMQ 4.x использует Khepri вместо Mnesia. При переполнении диска WAL не может записаться → Khepri сбрасывает состояние → пользователи исчезают. `RABBITMQ_DEFAULT_USER/PASS` применяются только при **первом старте** с пустым volume — повторный запуск их не восстанавливает. **Решение:** 1. `docker system prune` — освободить место на диске (Docker VM sparse disk не освобождает место автоматически). 2. Добавить `RABBITMQ_ERLANG_COOKIE` в env rabbitmq (стабилизирует cookie через перезапуски). 3. При повреждённом volume — стереть и пересоздать: ```bash docker compose down docker volume rm zulip_zulip-rabbitmq docker compose up -d ``` ### Log rotation (обязательно!) Без ротации логи заполняют Docker VM (~6 ГБ за несколько месяцев). `docker-compose.yml` — добавить к каждому сервису: ```yaml # zulip: logging: driver: json-file options: max-size: "50m" max-file: "5" # rabbitmq, memcached, redis: logging: driver: json-file options: max-size: "20m" max-file: "3" ``` ### Docker VM sparse disk Mac Docker Desktop использует sparse virtual disk. Место, освобождённое внутри VM, не возвращается хосту автоматически. `docker system prune` запускает compaction. --- ## Obsidian MCP Везде используется `mcpvault` (не `obsidian-mcp`). | Конфиг | Путь | |--------|------| | Claude Code CLI | `~/.claude/.mcp.json` | | Claude Desktop App | `~/Library/Application Support/Claude/claude_desktop_config.json` | | Hermes | встроен через toolset | Пример конфига (одинаковый для обоих): ```json { "mcpServers": { "obsidian": { "command": "mcpvault", "args": ["/Users/admin/obsidian"] } } } ``` **Pitfall:** `obsidian-mcp` (npm) был удалён — если остался в конфиге, Claude падает с "Failed to spawn process". Проверить логи: `~/Library/Logs/Claude/mcp-server-obsidian.log`. --- ## Схема маршрутизации сообщений ``` Zulip Events API ↓ poll (credentials Eagle) [zulip-router] (Go, Docker) │ ├── (без @mention, тред закреплён) → trigger=owner → POST боту ├── (новый тред без @mention) → trigger=default → POST default-боту │ └── @mention → ownership обновляется, НО НЕ форвардит (бот получит через Zulip outgoing webhook напрямую) Zulip Outgoing Webhook (при @mention) │ ├── @Орёл → POST host.docker.internal:8644/webhooks/eagle ├── @Кит → POST host.docker.internal:8645/webhooks/whale ├── @Валера → POST balda-agent:8091 └── @Клавдий → POST claudio-agent:8092 ``` ### Кто что получает | Компонент | @mention (Zulip outgoing webhook) | Без @mention (через zulip-router) | Шлёт ответы через | |-----------|----------------------------------|-----------------------------------|-------------------| | **Eagle (Hermes gateway)** | POST `/webhooks/eagle` напрямую | POST `/webhooks/eagle` (trigger=owner/default) | Zulip API (бот `eagle-bot`) | | **Кит (Hermes gateway)** | POST `/webhooks/whale` напрямую | POST `/webhooks/whale` (trigger=owner/default) | Zulip API (бот `whale-bot`) | | **Валера (balda-agent)** | POST `:8091` напрямую | POST `:8091` (trigger=owner/default) | Zulip API (бот `valera-bot`) | | **Клавдий (claudio-agent)** | POST `:8092` напрямую | POST `:8092` (trigger=owner/default) | Zulip API (бот `claudio-bot`) | **Два пути доставки:** 1. **@mention** — Zulip сам шлёт outgoing webhook на webhook URL бота напрямую (минуя роутер). Роутер видит это сообщение в event queue, обновляет ownership, но НЕ форвардит (бот уже получил). 2. **Без @mention** — если тред закреплён за ботом (ownership), роутер форвардит. Если треда нет — default-боту. **Важно:** Eagle и Кит НЕ регистрируют свои event queue в Zulip. Сообщения без @mention приходят через zulip-router. Роутер использует credentials Eagle для поллинга Zulip Events API, но Eagle сам в Zulip не поллит. ### Что не так (исторически, сейчас пофиксено) Роутер после установки ownership шлёт ВСЕ сообщения из треда владельцу, даже если @mention адресован другому боту. **Должно быть:** если @mention другого бота — ownership обновляется, НО НЕ форвардить. Бот получит сообщение через свой outgoing webhook (от Zulip напрямую). **Текущее состояние (2026-06-17):** ownership обновляется через @mention от другого бота, но НЕ форвардит — `processEvent` проверяет `!senderIsBot && !isOtherBotMention()` для mention-сообщений. Плюс guard `skip_bot_messages: true` отсекает все бот-сообщения целиком. --- ## Eagle: интеграция с zulip-router (2026-06-17) ### Архитектура У Eagle **два пути получения сообщений:** 1. **@mention (`@**Орёл**`)** → Zulip outgoing webhook → POST напрямую на `host.docker.internal:8644/webhooks/eagle` (минуя zulip-router). Eagle принимает через `WebhookAdapter` в `webhook.py`. 2. **Без @mention, тред закреплён** → zulip-router → trigger=owner → POST `host.docker.internal:8644/webhooks/eagle` Ответ уходит в Zulip напрямую через Zulip API (бот `eagle-bot`), **в тот же стрим и топик** откуда пришло сообщение — chat_id выводится в `_deliver_cross_platform` из `payload.display_recipient::subject`. **Важно:** Eagle НЕ запускает polling Zulip Events API (`ZULIP_POLLING_DISABLED=true`). ### Конфиг Eagle (актуальный) `~/.hermes/config.yaml` — секция `platforms`: ```yaml platforms: webhook: enabled: true extra: host: "0.0.0.0" port: 8644 routes: eagle: secret: INSECURE_NO_AUTH prompt: '{{message.content}}' deliver: zulip ``` - `host: "0.0.0.0"` — слушаем на всех интерфейсах, чтобы Docker (zulip-router через `host.docker.internal`) мог достучаться - `secret: INSECURE_NO_AUTH` — Zulip 10.x outgoing webhook не шлёт HMAC-подпись в заголовках. Safety rail на non-loopback снят патчем в коде - `deliver: zulip` — ответ перенаправляется в Zulip платформу - chat_id выводится из payload: `message.display_recipient::message.subject` (код в `_deliver_cross_platform`) ### Изменения в коде Hermes В `gateway/platforms/webhook.py` (закоммичено `d4e98a8b0`): 1. `_BUILTIN_DELIVER_PLATFORMS` — добавлен `"zulip"` 2. Safety rail INSECURE_NO_AUTH на non-loopback — убран 3. `_validate_signature` — поддержка Zulip token в JSON body (+ gzip-декодирование) 4. `_deliver_cross_platform` — при `deliver=zulip` и отсутствии `deliver_extra.chat_id` chat_id выводится из `payload.message.display_recipient::message.subject` 5. Debug-логи: HEADERS, ZULIP_RAW_BODY, cross-platform diagnostics ### Контекст сессий (webhook) **Проблема:** Каждое сообщение через webhook получало уникальный `chat_id = webhook:eagle:{delivery_id}`, поэтому каждое @mention создавало новую сессию без истории. **Решение (2026-06-17):** Два уровня фикса: 1. **Hermes webhook.py** (`gateway/platforms/webhook.py`) — `session_chat_id` теперь определяется приоритетно: - `X-Chat-Id` заголовок (ставится zulip-router как `stream::topic`) - Zulip outgoing webhook payload (`message.display_recipient::subject`) - fallback — delivery_id (для non-Zulip webhook-ов) 2. **zulip-router** (`forwarder.go`) — при POST добавляет заголовок `X-Chat-Id: stream::topic` Это покрывает оба пути доставки: - **через роутер** (без @mention) — `X-Chat-Id` от роутера - **напрямую от Zulip** (@mention) — Hermes сам определяет `stream::topic` из payload Важно: у разных ботов (Eagle vs Кит) разный `route_name` в `session_chat_id`, поэтому их сессии не смешиваются даже при одинаковом `stream::topic`. Git: `forwarder.go` — в репо роутера. `webhook.py` — патч в vendor Hermes (не коммитится). ### Управление Только через Eagle Dashboard: - `http://localhost:8880` (localhost) - `POST /api/services/hermes/start|stop|restart` --- ## Связанные страницы - [[claude-python-cli-proxy]] — Python Claude CLI proxy (claude-code-openai-wrapper, порт 8090) - [[tech/hermes-docker-kraken]] — Hermes на Кракене (Docker) - [[tech/kraken-network]] — сетевая топология - [[concepts/hermes-deployment-patterns]] — сравнение трёх моделей деплоя Hermes