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

347 lines
21 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
> План. **Шаги 1-3 выполнены** (2026-09-26): образ собран, контейнер `balda-executor` Up (healthy),
> `claude 2.1.283` + `codex 0.157.1` внутри, монтирования проверены в обе стороны.
> Мост `agents-bridge` :8790 — под **launchd** (`com.agents.bridge`, `RunAtLoad` + `KeepAlive`),
> протестирован (auth 401, идемпотентность, сквозной вызов CLI).
> Шаг 4 — **код написан, не активирован** (нужен перезапуск дашборда).
> 🔴 Осталось: **логин CLI делает Alex сам**; активация секции «Agents»; шаг 3 (туннель);
> шаги 5-6.
> Часть [[personal/projects/balda/balda-mm-delegation-plan]] (Фазы 4-5).
> Предыдущее: [[personal/projects/balda/balda-kraken-mm-deploy]] (Фаза 3 ✅ — бот отвечает Alex).
---
## 1. Решения
| # | Вопрос | Решение |
|---|---|---|
| 1 | Папка для проектов исполнителя | `~/agents` |
| 2 | Подтверждения | Full access → `permissions.mode: allow_all` |
| 3 | Управление линками в `~/agents` | Новая секция «Agents» в Eagle Dashboard |
| 4 | Auth CLI (claude/codex) | на хосте, `~/cli-auth`, **вне** `~/agents` |
| 5 | Сеть Kraken → Mac | Cloudflare Tunnel (существующий cloudflared на Mac) |
| 6 | Провайдер исполнителя (открытый п.1) | `claude` — оба CLI стоят в образе, переключение через `EXECUTOR_PROVIDER` в `.env` |
---
## 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
▼
Cloudflare Tunnel: agents-bridge.qentra.top → localhost:8790
│ (существующий cloudflared на Mac, PID + LaunchAgent)
▼
Мост (Mac, FastAPI :8790)
│ idempotency key, таймаут, лимит 1-2 параллельных задач, shared secret
▼
Контейнер-исполнитель (Mac, Docker)
│ -v ~/agents:/agents:rw
│ -v ~/Developer/<proj>:/agents/<name>:rw ← внешние проекты
│ -v ~/cli-auth/claude:/root/.claude:rw ← auth на хосте
│ -v ~/cli-auth/codex:/root/.codex:rw ← auth на хосте
│ Node 24 + codex + claude + git
▼
diff + тесты + коммит → Балда → Mattermost
```
### Auth CLI на хосте
| Данные | Хост | В контейнере | Права |
|---|---|---|---|
| Claude auth | `~/cli-auth/claude` | `/root/.claude` | 0700 |
| Codex auth | `~/cli-auth/codex` | `/root/.codex` | 0700 |
| OAuth-токен Claude | `~/Docker/balda-executor/.env` | env | 0600 |
📌 **Авторизацию CLI делает Alex сам** (решение 2026-09-26). Копирование auth-файлов из
`~/.claude`/`~/.codex` в `~/cli-auth` **отклонено** — я эти файлы не читаю и не копирую.
До его логина любая задача вернёт `Not logged in · Please run /login` (это ответ Claude Code,
а не ошибка моста — конвейер при этом доказанно рабочий).
Токен `CLAUDE_CODE_OAUTH_TOKEN` в `.env` контейнера — запасной путь для headless без монтирования.
Пересборка образа и обновление CLI auth не сбрасывают. Токены вне `/agents` → агент их не видит.
---
## 4. Секция «Agents» в Eagle Dashboard
Правки: `main.py` (роуты) · `templates/index.html` (вкладка `?tab=agents`) ·
`services.yaml` (запись `agents-bridge`, `health.type: http`) · `health.py`.
| Функция | Что делает |
|---|---|
| Список | содержимое `~/agents`: имя, тип, размер, mtime, статус линка |
| Создать | `mkdir ~/agents/<name>` |
| Подключить внешний | запись в `agents-mounts.yaml` → `compose up -d` |
| Hardlink | `ln -F` для файлов, проверка `st_dev` |
| Отключить | убрать из манифеста + `up -d`; **файлы на диске целы** |
| Терминал | ссылка на вкладку `files` |
**API:**
```
GET /api/agents → список
POST /api/agents {name} → mkdir
POST /api/agents/{name}/mount {host_path} → bind + recreate
DELETE /api/agents/{name}/mount → отвязать (файлы целы)
POST /api/agents/{name}/hardlink {file, name} → ln -F со st_dev
GET /api/agents/{name}/usage → размер
```
**Не делает:** не удаляет файлы, не убивает процессы, не создаёт symlink на внешние цели.
---
## 5. Подтверждения
Режимы Balda: `allow_all` (выбран) · `ask` · `deny_all`.
`ask` поддержан **только для Telegram и Slackagent**; для Mattermost фейлится закрыто.
Сценарий «подтвердить из чата» требует: **A** — дописать `ask` в MM (~1-2 дня, расширение PR #8),
либо **B** — апрув в мосте (~0.5 дня). При `allow_all` не нужно; начинать с B, когда понадобится.
---
## 6. Шаги
### ✅ Шаг 1 ВЫПОЛНЕН (2026-09-26)
Артефакты на диске (все сверены и рабочие; черновики от 15:36 доведены до состояния «собрано и запущено»):
| Путь | Что | Состояние |
|---|---|---|
| `~/agents` | рабочая область (`admin:staff`) | ✅ монтируется в `/agents`, запись в обе стороны проверена |
| `~/cli-auth/{claude,codex}` | auth CLI, 0700, **вне** `~/agents` | ✅ монтируются в `/root/.claude`, `/root/.codex` |
| `~/Docker/balda-executor/Dockerfile` | `node:24-bookworm` + claude + codex + git + `tini` | ✅ исправлен (см. ниже) |
| `~/Docker/balda-executor/docker-compose.yml` | 3 тома, сеть `executor-net`, `build:` | ✅ исправлен (см. ниже) |
| `~/Docker/balda-executor/agents-mounts.yaml` | манифест внешних проектов (пустой список) | ✅ создан |
| `~/Docker/balda-executor/tools/render-mounts.py` | генератор `compose.override.yaml` | ✅ создан, `--check` работает |
| `~/Docker/balda-executor/compose.override.yaml` | **генерируется**, руками не править | ✅ сгенерирован |
| `~/Docker/balda-executor/.env` | 0600, `EXECUTOR_PROVIDER=claude` | ✅ создан (без секретов) |
**Что уже работает (проверено):**
```
balda-executor Up (healthy) balda-executor:0.1.0
claude → 2.1.283 (Claude Code)
codex → codex-cli 0.157.1
```
`~/agents` ↔ `/agents` пишется в обе стороны; рестарт контейнера состояние сохраняет;
auth-каталоги хоста видны внутри.
#### 🔴 Три исправленных дефекта черновика (не повторять)
| Дефект | Симптом | Фикс |
|---|---|---|
| `env_file: .env` без файла | `docker compose up` падает на старте | `.env` создаётся **первым** шагом, 0600 |
| Нет `compose.override.yaml` | `up` падает на монтировании несуществующего пути | `tools/render-mounts.py` запускается **до** `up` |
| `FROM node:24-bookworm` без явных CA | — | `ca-certificates` + `update-ca-certificates` в Dockerfile (условие работы HTTPS у CLI) |
#### ⚠️ На macOS этой машины — `docker-compose`, НЕ `docker compose`
Docker CLI 29.4.1 + buildx как плагин (`~/.docker/cli-plugins/`), а compose — **отдельный бинарь**
`/opt/homebrew/bin/docker-compose` (v5.1.3) с симлинком в `/opt/homebrew/lib/docker/cli-plugins/`.
`docker compose` → `docker: unknown command: docker compose`; `docker compose config --quiet` →
`unknown flag: --quiet`.
📌 **Все команды в этом проекте — через `docker-compose`**, включая `docker-compose config` (без `--quiet`).
#### Сборка образа (сработавшая команда)
```bash
cd ~/Docker/balda-executor
python3 tools/render-mounts.py --check # 0 записей, ошибок нет
python3 tools/render-mounts.py # → compose.override.yaml
docker-compose config --services # → executor (валидация)
docker buildx build --builder arm64builder --platform linux/arm64 \
--provenance=false --load -t balda-executor:0.1.0 .
docker-compose up -d
docker-compose restart # проверка переживания рестарта
```
⚠️ Билдер — `arm64builder` (colima, `docker-container` driver). Дефолтный билдер
(`default` → `/var/run/docker.sock`) в статусе `error` — не использовать.
⚠️ `deploy.resources.limits.memory: 8G` из черновика **убран**: в не-swarm compose
эта секция молча игнорируется, лимит создавал ложное ощущение защиты.
#### ✅ Логин CLI — зона Alex (не автоматизируется)
`~/cli-auth/claude` пуст, в `~/cli-auth/codex` только `tmp/`.
🔴 Копирование auth-файлов **отклонено Alex** («авторизую я, копировать из кредсы не надо»).
Не предлагать повторно, не читать `~/.claude/.credentials.json`, `~/.codex/auth.json`.
Признак незалогиненного исполнителя в ответе задачи: `Not logged in · Please run /login`.
### ✅ Шаг 2 ВЫПОЛНЕН (2026-09-26) — мост `agents-bridge`
Проект: `~/Developer/agents-bridge/` (FastAPI, python из `~/Developer/eagle-dash/.venv`, py3.14).
| Файл | Роль |
|---|---|
| `app.py` | роуты, авторизация, лимит параллельности, ThreadPoolExecutor |
| `store.py` | SQLite WAL: `tasks` + UNIQUE-индекс по `idempotency_key` |
| `executor.py` | `docker exec` в контейнер, провайдеры claude/codex, таймаут, обрезка вывода |
| `config.py` | чтение `.env` (минимальный парсер, без python-dotenv на старте) |
| `.env` | 0600, `BRIDGE_SECRET` (64 симв.), **не коммитить** |
| `.env.example` | шаблон без секретов |
| `state.sqlite3` | статусы задач, WAL |
**API:**
```
GET /healthz → статус моста + контейнера (без секрета)
POST /task → 202 {task_id, status} | 200 (идемпотентный повтор) | 401 | 429 | 400
GET /task/{task_id} → статус + stdout/stderr/exit_code
GET /tasks?limit=N → последние задачи
```
**Авторизация:** заголовок `X-Bridge-Secret` на всех `/task*`. Идемпотентность — заголовок
`Idempotency-Key`; повтор возвращает **тот же** `task_id` со HTTP **200** (первый ответ — 202).
**Проверено живьём (2026-09-26):**
| Проверка | Результат |
|---|---|
| `POST /task` без секрета / с неверным | 401 / 401 ✅ |
| Два POST с одним `Idempotency-Key` | один `task_id` ✅ |
| Задача дошла до CLI в контейнере | ответ CLI вернулся ✅ |
| Статус в SQLite (WAL) | ✅ |
| `/healthz` видит контейнер | `container_running: true` ✅ |
Ответ `/healthz`:
```json
{"status":"ok","container":"balda-executor","container_running":true,
"provider":"claude","secret_configured":true,"active_tasks":0,"max_concurrent":1}
```
Результат сквозной пробы (до логина CLI): `status=failed, exit_code=1,
stdout='Not logged in · Please run /login'` — **это ожидаемо** и доказывает, что
путь Kraken → Mac → контейнер → CLI проходит целиком.
**Запуск (вручную, до launchd):**
```bash
cd ~/Developer/agents-bridge
~/Developer/eagle-dash/.venv/bin/python -m uvicorn app:app --host 127.0.0.1 --port 8790
```
### ✅ Шаг 3 ВЫПОЛНЕН (2026-09-26) — мост под launchd
Плейт `com.agents.bridge`: `~/Library/LaunchAgents/com.agents.bridge.plist`,
`RunAtLoad` + `KeepAlive`, `WorkingDirectory=/Users/admin/Developer/agents-bridge`,
логи в `~/Library/Logs/agents-bridge.{log,err.log}`.
Блокер шага 3 (туннель вёл бы в закрытый порт) снят: мост поднимается при входе и
перезапускается при падении.
```bash
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.agents.bridge.plist
launchctl list | grep agents.bridge # → 3063 0 com.agents.bridge
```
📌 **launchd не читает shell `PATH`** — в плейт добавлен `EnvironmentVariables.PATH`
с `/opt/homebrew/bin` (иначе `executor.py` не найдёт `docker`).
⚠️ Тест `KeepAlive` (убить pid → убедиться, что поднялся) **не выполнен**: требовал
`kill -9` и был отклонён. Штатная проверка — `launchctl kickstart -k`.
#### 🔴 Питфолл: `BRIDGE_SECRET` и redaction Hermes
Команда, создающая `.env` через `python -c` с f-строкой `BRIDGE_SECRET={...}`, в логе выглядит
обрезанной (`E...cret written`) — **это редакция секретов Hermes, файл на диске цел**.
Проверять содержимое не чтением файла, а через awk с маскировкой:
```bash
/usr/bin/awk -F= '{ if ($1 ~ /SECRET|TOKEN|KEY|PASSWORD/) print $1"=<redacted len "length($2)">"; else print }' .env
```
`read_file` на `.env` отказывает по определению (secret-bearing file).
**3. Сеть (~1 ч)** — Cloudflare Tunnel
Добавить published route `agents-bridge.qentra.top` → HTTP `localhost:8790` +
Access Application. Токен и процесс cloudflared не трогаются. Проверка: `curl` извне на `/healthz`.
⚠️ hostname — **одноуровневый** (двухуровневые дают TLS handshake failure).
📌 Alex делает это сам в панели Cloudflare Zero Trust (API-доступа к CF у агента нет).
Блокер снят: мост теперь под launchd (шаг 3), URL не будет вести в закрытый порт.
**4. Секция «Agents» — код ГОТОВ, не активирован (2026-09-26)**
`~/Developer/eagle-dash/agents.py` (роутер, 8 эндпоинтов) + `main.py`
(`app.include_router`) + `services.yaml` (запись `agents_bridge`).
Aктивация:
```bash
rm -rf ~/Developer/eagle-dash/__pycache__ && \
launchctl kickstart -k gui/$(id -u)/com.eagle.dashboard
```
Детали, питфоллы и спорный момент с `type: daemon` (конфликт с launchd моста за порт) —
[[personal/tech/eagle-dashboard-agents-tab]]. Вкладка `?tab=agents` в шаблоне **ещё не сделана**.
**5. Связка Балда → мост (~2 ч)**
MCP-инструмент в Балде → `POST /task`; проба `echo ok > /agents/test/probe.txt`.
**6. Приёмка (~1 день)**
Сквозной тест **от аккаунта Alex** в MM: `@balda` → задача → diff в ответе.
Замер расходов (DeepSeek + подписка), документация отката.
---
## 7. Риски
| Риск | Оценка | Митигация |
|---|---|---|
| Мост опубликован в интернет | высокий | Access Application + shared secret |
| Полный доступ агента к `~/agents` | средний | бэкапы; `git rm`-семантика вместо `rm` |
| Бан подписки за «unusual traffic» | средний | 1-2 параллельных задачи, официальный бинарь, без прокси токена |
| Hardlink через ФС → `EXDEV` | высокий | проверка `st_dev` в UI |
| Mac спит → туннель и мост недоступны | средний | задачи в durable-очереди Балды, идемпотентность |
---
## 8. Открыто
1. ~~Провайдер исполнителя~~ → **claude** (оба в образе, переключение `EXECUTOR_PROVIDER`)
2. ~~Нужен ли `ask`-апрув~~ → при `allow_all` не нужен; при потребности начинать с B (апрув в мосте, ~0.5 дня)
3. 🔴 **Логин CLI — делает Alex сам.** Копирование auth-файлов отклонено; до логина задачи
возвращают `Not logged in`
4. ~~Мост не в launchd~~ → ✅ **закрыто**: `com.agents.bridge` (`RunAtLoad` + `KeepAlive`)
5. 🔴 **Активация секции «Agents»** — код готов, нужен `__pycache__` + `kickstart -k` (апрув не получен)
6. ⚠️ **Конфликт за порт 8790** — `agents_bridge` в `services.yaml` объявлен `type: daemon`,
но мост уже управляется своим launchd. Ревизия при активации: `type: system` +
`health.type: launchctl` либо `type: none`. См. [[personal/tech/eagle-dashboard-agents-tab]] §4
7. ⚠️ Вкладка `?tab=agents` в `templates/index.html` не сделана — API готов, UI нет
8. Hostname моста — `agents-bridge.qentra.top` (шаг 3, public route добавляет Alex в CF)
9. Порт `8790` — свободен до старта моста, занят после; иных слушателей нет
10. ⚠️ Под launchd `agents.py` не найдёт `docker`/`docker-compose` (PATH) — если mount вернёт 500,
заменить литералы на `/opt/homebrew/bin/...`. Живьём не проверено (дашборд не перезапущен)
## Связанные заметки
- [[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/agents-bridge]] — мост :8790, API, питфоллы (шаг 2 ✅)
- [[personal/tech/eagle-dashboard-agents-tab]] — секция «Agents»: код, API, питфоллы (шаг 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` — архитектура дашборда
## История
- **2026-09-26**: план создан; шаги 1-2 выполнены (образ, контейнер, мост на :8790).
- **2026-09-26** (продолжение): мост переведён под launchd (шаг 3 ✅, `com.agents.bridge`);
написан код секции «Agents» (шаг 4, не активирован); логин CLI оставлен за Alex.
Туннель (шаг 3 сети) — за Alex, в панели Cloudflare Zero Trust.