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

17 KiB
Raw Blame History

title, type, namespace, tags, created, updated, last_synced, confidence
title type namespace tags created updated last_synced confidence
Hermes на Eagle (Mac M4 Max) — Настройка и подводные камни reference work
hermes
mac
eagle
claude-proxy
zulip
pitfalls
2026-05-21 2026-05-22 2026-05-22 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 (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 ничего не получает.

Установка

npm install -g openclaw-claude-proxy

Wrapper-скрипт (обязателен для launchd)

~/.local/bin/claude-proxy-start.sh:

#!/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 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>
launchctl load ~/Library/LaunchAgents/ai.claude-proxy.plist

Pitfall при перезагрузке: если старый процесс ещё держит порт 3456:

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):

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 — стереть и пересоздать:
    docker compose down
    docker volume rm zulip_zulip-rabbitmq
    docker compose up -d
    

Log rotation (обязательно!)

Без ротации логи заполняют Docker VM (~6 ГБ за несколько месяцев).

docker-compose.yml — добавить к каждому сервису:

# 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

Пример конфига (одинаковый для обоих):

{
  "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:

platforms:
  webhook:
    extra:
      port: 8646  # было 8645

Шаг 2. Убить второй gateway (PID 96351)

Это дубль — запущен руками из того же конфига, сидит на 8644:

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):

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):

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
  • Проверить @**Орёл** через роутер

Связанные страницы