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

18 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
3 Сеть Kraken → Mac ✅ LAN напрямую, туннель не нужен
4 Секция «Agents» в Dashboard ✅ активирована и проверена
5 Связка Балда → мост 🟡 связка работает, конфиг Балды ждёт переключения на type: http
6 Приёмка от аккаунта 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 ✅ LAN напрямую (http://192.168.1.57:8790); Cloudflare Tunnel — резерв
6 Провайдер исполнителя claude (дефолт в .env); codex собирается в том же образе, переключается переменной

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 — ✅ РЕШЕНО, туннель НЕ нужен

Итог: обычный HTTP по локальной сети

Kraken и Mac стоят в одной LAN и видят друг друга напрямую — проверено ping с Kraken на 192.168.1.57 (RTT ~2.4 мс, 0% потерь). Никакой туннель, никакой проброс и никакая Access Application не нужны.

BRIDGE_URL=http://192.168.1.57:8790

Проверки, подтвердившие связь:

Проверка Результат
ping Kraken → 192.168.1.57 2/2, RTT 2.4 мс ✅
curl с Kraken на :8790/healthz status: ok ✅
curl из контейнера в mattermost_mm-net на :8790/healthz status: ok ✅

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

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

📌 Если понадобится туннель в будущем — сначала проверить 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 — сейчас там stdio-вариант с docker run, который не может работать (нет docker.sock и нет shell в образе Балды). Нужен type: http → http://balda-executor-mcp:8890/mcp. См. personal/tech/balda-executor-mcp §7.
  2. Проверить, что инструменты видны в сессии Балды (после перезапуска).
  3. Логин CLI (Alex вручную) — без него сквозная приёмка невозможна.
  4. Cloudflare Tunnel — не нужен на текущей схеме; держать как резерв, если Mac уедет из LAN.

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

Компонент Где Статус
MCP-сервер balda-executor-mcp ~/Developer/balda-executor-mcp/ ✅ код + образ
Образ balda-executor-mcp:0.1.0, загружен на Kraken ✅
Контейнер MCP Kraken, /home/kraken/mcp/docker-compose.yml ✅ Up
Конфиг Балды runtime.mcp_servers.executor-mcp + balda.mcp_servers 🟡 ждёт type: http
.env Балды + BRIDGE_URL, + BRIDGE_SECRET ✅

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

Проверено живьём: MCP-handshake (stdio и HTTP), tools/list с 3 инструментами, executor_health из docker-сети Kraken вернул живые данные моста на Mac, Балда после правки конфига стартовала без ошибок.

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

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