Files
obsidian-vault/family/how-to/hermes-eagle-mac.md
T

344 lines
15 KiB
Markdown
Raw 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.
# 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
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>ai.claude-proxy</string>
<key>ProgramArguments</key>
<array>
<string>/Users/admin/.local/bin/claude-proxy-start.sh</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>HOME</key><string>/Users/admin</string>
</dict>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>StandardOutPath</key>
<string>/Users/admin/.hermes/logs/claude-proxy.log</string>
<key>StandardErrorPath</key>
<string>/Users/admin/.hermes/logs/claude-proxy.log</string>
</dict>
</plist>
```
```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