452 lines
17 KiB
Markdown
452 lines
17 KiB
Markdown
---
|
||
title: Hermes на Eagle (Mac M4 Max) — Настройка и подводные камни
|
||
type: reference
|
||
namespace: work
|
||
tags:
|
||
- hermes
|
||
- mac
|
||
- eagle
|
||
- claude-proxy
|
||
- zulip
|
||
- pitfalls
|
||
created: '2026-05-21'
|
||
updated: '2026-05-22'
|
||
last_synced: '2026-05-22'
|
||
confidence: 0.9
|
||
---
|
||
# Hermes на Eagle (Mac M4 Max) — Настройка и подводные камни
|
||
|
||
Hermes работает нативно (не в Docker) на Mac через `hermes gateway`.
|
||
Транспорт — Zulip (запущен в Docker). Провайдер модели — openclaw-claude-proxy
|
||
(см. ниже).
|
||
|
||
## Компоненты
|
||
|
||
| Компонент | Расположение | Запуск |
|
||
|-----------|-------------|--------|
|
||
| Hermes config | `~/.hermes/config.yaml` | — |
|
||
| 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)
|
||
│
|
||
├── @Валера → trigger=owner → POST balda-agent:8091
|
||
├── @Клавдий → trigger=owner → POST claudio-agent:8092
|
||
├── @Орёл → trigger=owner → POST host.docker.internal:8645/webhooks/eagle
|
||
├── @Кит → trigger=owner → POST host.docker.internal:8646/webhooks/whale
|
||
└── ответы от ботов → в Zulip (боты пишут сами через API)
|
||
```
|
||
|
||
### Кто что получает
|
||
|
||
| Компонент | Получает сообщения через | Шлёт ответы через |
|
||
|-----------|-------------------------|-------------------|
|
||
| **Eagle (Hermes gateway)** | zulip-router → POST `/webhooks/eagle` | Zulip API напрямую (бот `eagle-bot`) |
|
||
| **Кит (Hermes gateway)** | zulip-router → POST `/webhooks/whale` | Zulip API напрямую (бот `whale-bot`) |
|
||
| **Валера (balda-agent)** | zulip-router → POST `/zulip/webhook` | Zulip API напрямую (бот `valera-bot`) |
|
||
| **Клавдий (claudio-agent)** | zulip-router → POST `/zulip/webhook` | Zulip API напрямую (бот `claudio-bot`) |
|
||
|
||
**Важно:** `@**Орёл**` и `@**Кит**` НЕ регистрируют свои event queue в Zulip.
|
||
Все сообщения приходят **только через zulip-router**. Роутер использует
|
||
credentials Eagle для поллинга Zulip Events API, но Eagle сам в Zulip
|
||
не поллит.
|
||
|
||
### Что не так (проблема trigger=owner)
|
||
|
||
Роутер после установки ownership шлёт ВСЕ сообщения из треда владельцу,
|
||
даже если @mention адресован другому боту. Пример:
|
||
|
||
1. Пользователь пишет `@Валера привет` → ownership = Валера
|
||
2. Пользователь пишет `@Кит а как там` → роутер всё ещё шлёт Валере
|
||
(trigger=owner), хотя @mention адресован Киту
|
||
|
||
**Должно быть:** если @mention другого бота — ownership обновляется,
|
||
НО НЕ форвардить. Бот получит сообщение через свой outgoing webhook
|
||
(от Zulip напрямую).
|
||
|
||
---
|
||
|
||
## План работ: миграция портов Eagle и Whale (2026-06-17)
|
||
|
||
### Текущее состояние
|
||
|
||
Eagle и Кит (Whale) — два отдельных gateway на одном Mac. Оба читают
|
||
один `config.yaml` (`~/.hermes/`). Кит сидит на порту 8645, Eagle — на 8644
|
||
(занял свободный, так как 8645 был занят).
|
||
|
||
Нужно: Eagle → 8645, Кит → другой порт.
|
||
|
||
### Шаг 1. Сменить порт Кита
|
||
|
||
Файл: `~/.hermes/hermes-whale/config.yaml`
|
||
|
||
Поменять порт webhook с 8645 на 8646:
|
||
|
||
```yaml
|
||
platforms:
|
||
webhook:
|
||
extra:
|
||
port: 8646 # было 8645
|
||
```
|
||
|
||
### Шаг 2. Убить второй gateway (PID 96351)
|
||
|
||
Это дубль — запущен руками из того же конфига, сидит на 8644:
|
||
|
||
```bash
|
||
kill 96351
|
||
```
|
||
|
||
PID актуальный — проверить перед kill.
|
||
|
||
### Шаг 3. Перезапустить Eagle на 8645
|
||
|
||
Через Eagle Dashboard (или `launchctl kickstart ai.hermes.gateway` — только
|
||
по явной команде).
|
||
|
||
### Шаг 4. Проверить
|
||
|
||
1. `curl -s http://127.0.0.1:8645` → 200 (Eagle жив)
|
||
2. `curl -s -X POST http://127.0.0.1:8645/webhooks/eagle -d '{"message":"test"}'`
|
||
→ 202 Accepted
|
||
3. `@**Орёл**` в Zulip → Eagle отвечает
|
||
4. Роутер шлёт на `host.docker.internal:8645/webhooks/eagle` — проверить логи
|
||
|
||
---
|
||
|
||
---
|
||
|
||
### Шаг 5. Поправить роутер — route `/webhooks/whale`
|
||
|
||
В конфиге роутера (`~/Docker/zulip-router/config.yaml`) добавить бота `Кит`
|
||
с webhook на новый порт (8646):
|
||
|
||
```yaml
|
||
bots:
|
||
- name: "Кит"
|
||
aliases: ["Whale", "whale", "кит"]
|
||
webhook: "http://host.docker.internal:8646/webhooks/whale"
|
||
```
|
||
|
||
### Шаг 6. Проверить @mention всех ботов
|
||
|
||
- `@**Орёл**` → Eagle отвечает
|
||
- `@**Кит**` → Кит отвечает
|
||
- `@**Валера**` → Валера отвечает (через роутер)
|
||
- `@**Клавдий**` → Клавдий отвечает (через роутер)
|
||
|
||
### Шаг 7. Обновить доку zulip-router.md
|
||
|
||
Записать:
|
||
- Актуальную схему роутинга с портами
|
||
- Статус TODO (проверено/не проверено)
|
||
- Проблему с trigger=owner
|
||
|
||
---
|
||
|
||
## Что сделано по zulip-router (история)
|
||
|
||
### 2026-06-16 — деплой zulip-router
|
||
|
||
- Написан код роутера (`~/Developer/zulip-router/`, Go, ~260 LOC)
|
||
- Создан compose (`~/Docker/zulip-router/docker-compose.yaml`)
|
||
- Сеть `balda_default` (external) — чтобы видел balda-контейнеры
|
||
- `.env` с API key Eagle (`eagle-bot@zulip.qentra.top`)
|
||
- `docker-compose up -d` — запущен
|
||
- Events API polling отключён у обоих balda-ботов (`events_polling.enabled: false`)
|
||
|
||
### 2026-06-17 — отладка
|
||
|
||
- Eagle добавлен в конфиг роутера как бот, webhook на `:8644`
|
||
- Кит добавлен в конфиг роутера как бот, webhook на `:8644`
|
||
- Выяснено: роутер форвардит все сообщения из треда с ownership,
|
||
даже если @mention адресован другому боту (проблема trigger=owner)
|
||
- Актуальный код роутера в контейнере не совпадает с кодом в репе
|
||
|
||
### Статус проблем
|
||
|
||
- [ ] **trigger=owner** — роутер не фильтрует @mention другого бота.
|
||
Должен: если @mention другого бота — ownership ставится, но НЕ форвардить.
|
||
- [ ] **Код в репе vs контейнере** — рассинхронизация.
|
||
Надо синхронизировать: `~/Developer/zulip-router/` → контейнер.
|
||
|
||
---
|
||
|
||
## Связанные страницы
|
||
|
||
Eagle использует webhook platform для приёма сообщений от zulip-router.
|
||
Напрямую в Zulip через Events API Eagle **не подписан** — все сообщения
|
||
приходят через роутер.
|
||
|
||
### Конфигурация
|
||
|
||
`~/.hermes/config.yaml` (секция `platforms`):
|
||
|
||
```yaml
|
||
platforms:
|
||
webhook:
|
||
enabled: true
|
||
extra:
|
||
host: 127.0.0.1
|
||
port: 8645
|
||
routes:
|
||
eagle:
|
||
secret: INSECURE_NO_AUTH
|
||
prompt: '{{message.content}}'
|
||
deliver: zulip
|
||
```
|
||
|
||
### Порт 8645
|
||
|
||
Eagle слушает webhook на **127.0.0.1:8645**. Это **единственный** порт Eagle.
|
||
|
||
### Схема работы
|
||
|
||
```
|
||
Zulip
|
||
└── Events API → [zulip-router] (Go, Docker)
|
||
├── @Валера → POST balda-agent:8091
|
||
├── @Клавдий → POST claudio-agent:8092
|
||
└── @Орёл / @Кит → POST host.docker.internal:8645/webhooks/{eagle,whale}
|
||
```
|
||
|
||
- Роутер использует credentials Eagle (`eagle-bot@zulip.qentra.top`)
|
||
- Eagle принимает сообщения **только через webhook** — не через Zulip поллинг
|
||
- `@**Орёл**` в Zulip → роутер подхватывает → POST `/webhooks/eagle` → Eagle отвечает
|
||
|
||
### Проблема (2026-06-17)
|
||
|
||
Eagle не отвечал на `@**Орёл**`. Причины:
|
||
|
||
1. **Второй gateway (PID 96351)** был запущен вручную из того же `~/.hermes/` и
|
||
занял порт 8644, так как 8645 был уже занят Китом (PID 90402).
|
||
2. Кит (Whale) по ошибке сидел на порту 8645 — порту Eagle.
|
||
3. Роутер слал на `:8644`, а Eagle ждал на `:8645`.
|
||
|
||
**Что сделано:**
|
||
- Конфиг Eagle приведён к состоянию: порт 8645, webhook с route `eagle`
|
||
- Второй gateway (PID 96351) — удалить (дубликат)
|
||
- Кита пересадить на другой порт (не 8645)
|
||
|
||
### TODO
|
||
|
||
- [ ] Пересадить Кита с 8645 на другой порт (8646?)
|
||
- [ ] Убить PID 96351 (дубль)
|
||
- [ ] Перезапустить Eagle на 8645
|
||
- [ ] Проверить `@**Орёл**` через роутер
|
||
|
||
---
|
||
|
||
## Связанные страницы
|
||
|
||
- [[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
|