Files
obsidian-vault/personal/plans/balda-mm-phase4-agents.md
T

25 KiB
Executable File
Raw Blame History

Фаза 4 — исполнитель на Mac + ~/agents + секция «Agents» в Eagle Dashboard

План + статус. Старт 2026-09-26. Часть personal/projects/balda/balda-mm-delegation-plan (Фазы 4-5). Предыдущее: personal/projects/balda/balda-kraken-mm-deploy (Фаза 3 ✅ — бот отвечает Alex).


0. Статус шагов

# Шаг Статус
1 ~/agents + ~/cli-auth + контейнер ✅ собрано, запущено
2 Мост FastAPI :8790 ✅ работает под launchd, туннель agents-bridge.qentra.top заведён Alex'ом
3 Сеть Kraken → Mac ✅ Cloudflare Tunnel (постоянно); LAN остаётся для отладки
4 Секция «Agents» в Dashboard ✅ активирована и проверена
5 Связка Балда → мост ✅ конфиг type: http, образ 0.2.0, tools/list из docker-сети
6 Obsidian MCP для Балды ✅ контейнер obsidian-mcp на Kraken + type: sse в конфиге (транспорт проверен)
6a 🔴 Инструменты MCP в сессии Балды 🔴 блокер: снапшот каталога в state.db содержит только builtin → Балда «MCP не вижу». См. personal/tech/balda-executor-mcp §10g
7 Приёмка от аккаунта Alex 🔴 блокер: авторизация CLI

1. Решения

# Вопрос Решение
1 Папка для проектов исполнителя ~/agents
2 Подтверждения Full access → permissions.mode: allow_all
3 Управление линками в ~/agents Секция «Agents» в Eagle Dashboard
4 Auth CLI (claude/codex) на хосте, ~/cli-auth, вне ~/agents; логинит Alex вручную
5 Сеть Kraken → Mac 🟢 Cloudflare Tunnel agents-bridge.qentra.top — постоянный путь (Alex завёл маршрут); LAN 192.168.1.57:8790 — только для отладки
6 Провайдер исполнителя claude (дефолт в .env); codex собирается в том же образе, переключается переменной
7 Тип MCP-транспорта в конфиге Балды http (валидатор: stdio | http | sse); Obsidian — sse
8 Туннель для Obsidian MCP ❌ не нужен — контейнер стоя́т на Kraken рядом с Балдой, ходят по docker-сети

2. Ограничения macOS Docker (проверено тестами)

Ограничение Итог
Симлинк на внешний путь битый в контейнере — цель резолвится от корня контейнера
Live mount --bind внутри VM контейнер не видит (virtiofs строит маппинг на старте)
Сетевая шара SMB/NFS в контейнер не протянуть (тот же virtiofs)
Hardlink на каталог физически невозможен (ядро запрещает)
Hardlink через границу ФС EXDEV

Следствие: внешние проекты подключаются только bind-mount'ом через -v + docker-compose up -d (2-3 сек). Hardlink — только для файлов внутри одного тома.

Детали механизма: personal/tech/macos-docker-virtiofs-mount-limits.


3. Архитектура (собрано)

Mattermost (Alex)
   │  «почини баг X в project-y»
   ▼
Балда-оркестратор (Kraken, DeepSeek, allow_all)
   │  читает Obsidian, формирует ТЗ, /goalkeeper → work → validate
   ▼
MCP-сервер balda-executor-mcp (Kraken, контейнер в mattermost_mm-net) :8890
   │  инструменты executor_run / executor_status / executor_health
   │  HTTP + X-Bridge-Secret
   ▼
LAN: http://192.168.1.57:8790   ← Kraken и Mac в одной сети, туннель не нужен
   ▼
Мост (Mac, FastAPI :8790) — под launchd com.agents.bridge
   │  idempotency key, таймаут 900 с, лимит 1 задача, shared secret
   ▼
Контейнер-исполнитель balda-executor (Mac, Docker)
   │  -v ~/agents:/agents:rw
   │  -v ~/Developer/<proj>:/agents/<name>:rw    ← внешние проекты (override)
   │  -v ~/cli-auth/claude:/root/.claude:rw      ← auth на хосте
   │  -v ~/cli-auth/codex:/root/.codex:rw        ← auth на хосте
   │  Node 24 + claude 2.1.283 + codex 0.157.1 + git
   ▼
diff + тесты + коммит → Балда → Mattermost

Детали MCP-сервера: personal/tech/balda-executor-mcp.

Auth CLI на хосте

Данные Хост В контейнере Права
Claude auth ~/cli-auth/claude /root/.claude 0700
Codex auth ~/cli-auth/codex /root/.codex 0700

Пересборка образа и обновление CLI auth не сбрасывают. Токены вне /agents → агент их не видит.


4. Что собрано на диске

Путь Что
~/agents/ рабочая область исполнителя
~/cli-auth/{claude,codex} 0700, монтируются в контейнер
~/Docker/balda-executor/Dockerfile Node 24 + claude + codex + git + tini + CA
~/Docker/balda-executor/docker-compose.yml 4 тома, сеть executor-net, build-секция
~/Docker/balda-executor/.env EXECUTOR_PROVIDER=claude, 0600
~/Docker/balda-executor/agents-mounts.yaml манифест внешних проектов
~/Docker/balda-executor/tools/render-mounts.py манифест → compose.override.yaml
~/Developer/agents-bridge/ мост: app.py, store.py, executor.py, config.py
~/Library/LaunchAgents/com.agents.bridge.plist автозапуск + KeepAlive, --host 0.0.0.0
~/Developer/eagle-dash/agents.py роутер секции «Agents»
~/Developer/balda-executor-mcp/ MCP-сервер: server.py, Dockerfile, docker-compose.yml
/home/kraken/mcp/docker-compose.yml (Kraken) запуск контейнера MCP

Известные питфоллы (пройдены):

  • docker compose на Mac отсутствует — только docker-compose (v5.1.3); на Kraken наоборот: docker compose, а docker-compose → command not found;
  • compose.override.yaml обязателен при старте, иначе up падает на монтировании;
  • образ исполнителя: node:24-bookworm вместо scratch (CA-сертификаты — иначе TLS-фейл);
  • psutil не установлен в venv дашборда → supervisor не может трекать мост по PID; health для моста — type: http;
  • --host берётся из ProgramArguments plist, .env его не перебивает; смена аргументов требует bootout+bootstrap, не kickstart;
  • образ Балды из scratch: нет shell, docker exec … sh не работает; docker.sock в контейнер Балды не смонтирован → docker run изнутри невозможен;
  • на Kraken в sshd AllowTcpForwarding no → обратный SSH-туннель поставить нельзя.

5. Секция «Agents» в Eagle Dashboard

Правки: main.py (include_router) · agents.py (новый роутер) · templates/index.html (вкладка ?tab=agents) · services.yaml (сервис agents_bridge).

Функция Что делает
Список содержимое ~/agents: имя, размер, mtime, статус линка
Создать mkdir ~/agents/<name>
Подключить внешний запись в agents-mounts.yaml → render-mounts.py → up -d
Hardlink ln -F для файлов, проверка st_dev
Отключить убрать из манифеста + up -d; файлы на диске целы
Статус моста /api/agents/bridge — provider, container, active, limit, pid, secret
Терминал ссылка на вкладку files

API: GET/POST /api/agents · POST|DELETE /api/agents/{name}/mount · POST /api/agents/{name}/hardlink · GET /api/agents/{name}/usage · GET /api/agents/mounts · POST /api/agents/mounts/render · GET /api/agents/bridge.

Не делает: не удаляет файлы, не убивает процессы, не создаёт symlink на внешние цели.

Приёмка секции (2026-09-26) — 13 проверок, все прошли

Проверка Результат
создание агента 201 ✅
повторное создание 409 ✅
подключение внешнего проекта 200 + контейнер Recreated ✅
проект виден внутри контейнера да, ls /agents/testproj → файлы ✅
отвязка 200; внутри контейнера пусто, на хосте файлы целы ✅
hardlink на каталог 400 (ядро запрещает) ✅
hardlink на файл в том же томе 200, links=2, одинаковый inode ✅
слишком широкий host_path 400 ✅
размер каталога OK ✅
?tab=agents рендерится 200, 75 КБ ✅
JS-целостность шаблона скобки парны, 8 методов на месте ✅
Alpine в логе дашборда ошибок нет ✅

6. Мост agents-bridge

~/Developer/agents-bridge/. Запуск под launchd: com.agents.bridge, лог ~/Library/Logs/agents-bridge.err.log.

Эндпоинт Назначение
GET /healthz живость моста + контейнера
POST /task поставить задачу {prompt, provider?}
GET /task/{id} статус + stdout/stderr/exit_code
GET /tasks последние 20 задач

Безопасность: заголовок X-Bridge-Secret (иначе 401), Idempotency-Key (повтор возвращает тот же task_id), лимит 1 одновременная задача (иначе 429), таймаут 900 с, обрезка вывода 200 КБ.

Приёмка моста (2026-09-26) — 6 проверок, все прошли

Проверка Результат
без секрета / неверный секрет 401 / 401 ✅
два POST с одним Idempotency-Key одинаковый task_id ✅
задача дошла до CLI в контейнере да ✅
ответ CLI вернулся (Not logged in · Please run /login) да — это ответ Claude Code, конвейер рабочий ✅
состояние в SQLite (WAL) ✅
/healthz видит контейнер status: ok ✅

Блокер приёмки: CLI внутри контейнера не авторизован. Логинит Alex вручную: claude / codex внутри контейнера, credentials кладутся в ~/cli-auth/{claude,codex}.


7. Сеть Kraken → Mac — 🟢 Cloudflare Tunnel (постоянно), LAN — отладка

Итог: постоянный путь — туннель, не IP

🔴 Alex потребовал убрать LAN-IP из постоянного конфига — домашний IP не является стабильным адресом: сменится адрес Mac или уедет роутер, и MCP отвалится молча. Туннель даёт стабильное имя и не привязан к топологии сети.

BRIDGE_URL=https://agents-bridge.qentra.top

Маршрут agents-bridge.qentra.top → http://localhost:8790 заведён Alex'ом в Cloudflare Zero Trust. Проверено из docker-сети Kraken:

Проверка Результат
curl https://agents-bridge.qentra.top/healthz из mattermost HTTP 200, {"status":"ok","container":"balda-executor",…} ✅
заголовки ответа server: cloudflare, cf-ray: …-FRU, HTTP/2 200 ✅
ping Kraken → 192.168.1.57 (LAN, резерв) 2/2, RTT 2.4 мс ✅

⚠️ На момент записи Access Application на agents-bridge не задан — туннель отвечает 200 без авторизации. Пока защита — X-Bridge-Secret и непредсказуемое имя поддомена. Завести Access Application (или подтвердить, что она есть), иначе мост фактически открыт в интернет.

Что НЕ сработало и почему (чтобы не повторять)

Путь Почему отпал
Обратный SSH-туннель (ssh -R) 🔴 На Kraken в sshd стоит AllowTcpForwarding no. Симптом: Error: remote port forwarding failed for listen port 18790, в verbose — debug1: Remote: Server has disabled port forwarding. Не диагностировать как «порт занят»: порт свободен, форвард выключен администратором
LAN как постоянный адрес Alex против: IP не стабилен. Годится только для отладки

📌 Если понадобится туннель в будущем — сначала проверить AllowTcpForwarding на Kraken, потом уже ставить форвард. Cloudflare Tunnel этот запрет обходит (исходящее соединение, не SSH-форвард).

Мост теперь слушает всю сеть — это осознанное изменение

Было Стало Почему
BRIDGE_HOST=127.0.0.1 BRIDGE_HOST=0.0.0.0 иначе Kraken не достучится

⚠️ Порт 8790 теперь открыт в локальной сети. Защита — X-Bridge-Secret (обязателен, иначе POST /task отдаёт 503). При уходе Mac в недоверенную сеть — вернуть 127.0.0.1 и поднимать туннель.

🔴 Питфолл: .env не перебивает plist. Я поменял BRIDGE_HOST в .env, но мост продолжал слушать 127.0.0.1 — потому что --host жёстко задан в ProgramArguments plist. Симптом: lsof показывает 127.0.0.1:8790 после «перезапуска с новым конфигом». Исправлять надо в plist, а не в .env.

# применить: правка plist → перезагрузка сервиса (не kickstart!)
launchctl bootout   gui/$(id -u)/com.agents.bridge
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.agents.bridge.plist
# проверка: должно быть *:8790, а не 127.0.0.1:8790
lsof -nP -iTCP:8790 -sTCP:LISTEN

kickstart перечитывает только программу, но не изменённый ProgramArguments — для смены аргументов нужен bootout + bootstrap.


8. Риски

Риск Оценка Митигация
Мост опубликован в интернет высокий Access Application + shared secret
Полный доступ агента к ~/agents средний бэкапы; git rm-семантика вместо rm
Бан подписки за «unusual traffic» средний 1 параллельная задача, официальный бинарь, без прокси токена
Hardlink через ФС → EXDEV высокий проверка st_dev в API
Mac спит → туннель и мост недоступны средний задачи в durable-очереди Балды, идемпотентность

9. Открыто

  1. ✅ Конфиг Балды переведён на HTTP-MCP — type: http → http://balda-executor-mcp:8890/mcp. Плюс образ на Kraken обновлён до 0.2.0 (в 0.1.0 нет uvicorn → рестарт-петля). См. personal/tech/balda-executor-mcp §10e-10f.
  2. ✅ Obsidian прописан в конфиг Балды — контейнер obsidian-mcp на Kraken поднят, runtime.mcp_servers.obsidian добавлен. ⚠️ Тип — sse, не http:
    runtime:
      mcp_servers:
        executor-mcp:
          type: http
          url: "http://balda-executor-mcp:8890/mcp"
        obsidian:
          type: sse
          url: "http://obsidian-mcp:3101/sse"
    balda:
      mcp_servers:
        - executor-mcp
        - obsidian
    
    См. §11 ниже.
  3. 🔴 MCP-инструменты не видны в сессии Балды. Транспорты и сети проверены живьём (оба сервера отвечают 200 из docker-сети), Балда стартует без ошибок и отвечает в чате — но говорит «MCP не вижу». Корень найден: снапшот каталога в state.db (balda.appruntime_catalog_snapshot:*) содержит только builtin — источники configured не скомпилированы; плюс сессия закреплена за старым RuntimeSnapshotID. Разбор и механика — personal/tech/balda-executor-mcp §10g. ⏳ Ждёт решения Alex: A) вычистить ключи снапшотов из state.db (рекомендовано), B) полный сброс state.db. ⚠️ Тест-пост должен идти от админского токена — посты от имени самого бота Балда игнорирует (правильно): docker exec mattermost mmctl --local token generate admin ….
  4. 🔴 Логин CLI (Alex вручную) — без него сквозная приёмка невозможна.
  5. 🔴 Завести Access Application на agents-bridge.qentra.top — сейчас туннель отвечает 200 без авторизации (проверено). Защита — только X-Bridge-Secret.
  6. ⚠️ ~/Developer/balda-executor-mcp/docker-compose.yml в репо → 0.2.0; на Kraken compose лежит в /home/kraken/balda-executor-mcp/docker-compose.yml (был /home/kraken/mcp/ — путь изменился, старый compose мёртв).

9a. 🔴 Obsidian на Kraken — существовал ДО этой сессии

Важное исправление ошибочного вывода. Я дважды сказал Alex, что Obsidian MCP на Kraken нет / есть только на Mac. Оба утверждения неверны.

Почему ошибся: искал через docker ps --filter name=obsidian — контейнера нет. Но на Kraken Obsidian MCP запущен не контейнером, а как stdio-процесс внутри агента Hermes:

mcpvault /vault  (PID 1168741, UID 10000)
  └─ родитель PID 5039 = hermes gateway  (контейнер hermes-kraken)

Конфиг — /opt/data/config.yaml внутри hermes-kraken. Волт на Kraken реальный: /home/kraken/obsidian → /srv/…-49e8f586…/obsidian (с personal/, family/, .git/).

📌 Правило поиска: docker ps не является доказательством отсутствия сервиса. Сначала ps aux | grep <name>, потом порты (ss -tlnp), и только потом контейнеры.

10. Шаг 5 — связка Балда → мост (обновлено 2026-09-26)

Компонент Где Статус
MCP-сервер balda-executor-mcp ~/Developer/balda-executor-mcp/ ✅ код + образ 0.2.0
Образ на Kraken balda-executor-mcp:0.2.0 (0.1.0 оставлен для отката) ✅ загружен, хеш сверен
Контейнер MCP Kraken, /home/kraken/balda-executor-mcp/docker-compose.yml ✅ Up, tools/list = 3 инструмента
Конфиг Балды runtime.mcp_servers.{executor-mcp: http, obsidian: sse} ✅ залит
.env Балды BRIDGE_URL=https://agents-bridge.qentra.top + BRIDGE_SECRET ✅ туннель, не IP

Бэкапы на Kraken: config.yaml.bak-mcp-20260926-194313, config.yaml.bak-obsidian-20260926-214849, .env.bak-mcp-*, .env.bak-url-*.

Проверено живьём: /healthz = {"status":"ok",…}, tools/list из docker-сети отдаёт executor_run / executor_status / executor_health, туннель до моста отвечает 200 через Cloudflare.

📌 Ключевой вывод по архитектуре: MCP-сервер обязан быть отдельным контейнером с HTTP-транспортом. Вариант «stdio + docker run изнутри Балды» нерабочий: в образе Балды нет shell и не смонтирован docker.sock.

11. Obsidian MCP на Kraken — контейнер (сделан в этой сессии)

Балде нужен Obsidian не по туннелю: контейнер стоит рядом, в той же docker-сети. Туннель к Obsidian Alex заводить не надо — это была моя ошибка в предложении.

Что Значение
Проект ~/Developer/obsidian-mcp-kraken/ (Dockerfile)
Образ obsidian-mcp-image:0.1.0 (node + mcp-proxy + mcpvault)
Контейнер на Kraken obsidian-mcp, сеть mattermost_mm-net
Внутренний порт 3101 (--port 3101 -- mcpvault /vault), наружу не публикуется
Том /srv/dev-disk-by-uuid-49e8f586-…/obsidian:/vault:ro (только чтение)
Endpoint для Балды http://obsidian-mcp:3101/sse, type: sse

⚠️ Порт именно 3101, не 8080. mcp-proxy слушает порт из --port; проба на 8080 даёт Connection refused при живом контейнере. Проверять так:

# из контейнера в mattermost_mm-net (там есть curl)
docker exec mattermost sh -c 'curl -sS -m 8 -H "Accept: text/event-stream" http://obsidian-mcp:3101/sse'
# ожидаем: event: endpoint / data: /messages?sessionId=…

📌 mcpvault — только stdio. HTTP-режима у него нет, поэтому и нужен обёртка mcp-proxy (ENTRYPOINT mcp-proxy, CMD --port 3101 -- mcpvault /vault). Проверено на живом контейнере: mcp-proxy --port 3101 -- mcpvault /vault.

📌 У mcp-proxy нет флага --host — в отличие от Python-моста. Передавать только --port + --. Лишний --host роняет контейнер.

Связанные заметки