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

305 lines
18 KiB
Markdown
Executable File
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.
# Фаза 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`.
```bash
# применить: правка 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`.
## Связанные заметки
- [[personal/projects/balda/balda-mm-delegation-plan]] — общий план (Фазы 4-5)
- [[personal/projects/balda/balda-kraken-mm-deploy]] — Фаза 3 (✅)
- [[personal/plans/balda-mm-pr-plan]] — Фаза 2 (PR #8)
- [[personal/tech/balda-executor-mcp]] — MCP-сервер делегирования (шаг 5)
- [[personal/tech/agents-bridge]] — мост :8790 (шаг 2)
- [[personal/tech/eagle-dashboard-agents-tab]] — секция «Agents» в дашборде (шаг 4)
- [[personal/tech/macos-docker-virtiofs-mount-limits]] — ограничения macOS Docker
- [[personal/tech/balda-go-fx-pitfalls]] — fx-питфоллы, дельта-апплаер
- [[family/how-to/kraken-portainer-access]] — cloudflared, routes
- скилл `eagle-dashboard` — архитектура дашборда