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

339 lines
22 KiB
Markdown
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. Статус: **ПЛАН — ожидает апрува Alex, работа не начата.**
> Часть [[personal/projects/balda/balda-mm-delegation-plan]] (Фаза 4 + Фаза 5).
> Предыдущее: [[personal/projects/balda/balda-kraken-mm-deploy]] (Фаза 3 ✅ — бот отвечает Alex).
---
## 0. Решения Alex (2026-09-26, этот заход)
| # | Вопрос | Решение |
|---|---|---|
| 1 | Папка для проектов исполнителя | **`~/agents`** |
| 2 | Подтверждения | **Full access** → `permissions.mode: allow_all` |
| 3 | Управление hardlink/symlink в `~/agents` | **Новая секция в Eagle Dashboard** |
---
## 1. 🔴 ПРОВЕРЕНО ЖИВЫМ ТЕСТОМ: симлинк в Docker НЕ работает
Alex спросил: «Если в папку симлинк кинуть вовне — докер это прочтет?»
**Ответ: НЕТ.** Проверено на живом Docker (alpine:3.20, linux/arm64), не по памяти.
```
host: /tmp/symtest/outside/file.txt = "REAL CONTENT ON HOST"
bind: -v /tmp/symtest/bind:/mnt (внутри /mnt/target -> /tmp/symtest/outside)
контейнер: cat /mnt/target/file.txt
→ cat: can't open '/mnt/target/file.txt': No such file or directory
```
Сам симлинк внутри виден (`lrwxrwxrwx link -> /outside`), но **цель резолвится
относительно корня контейнера**, где этого пути нет. Docker монтирует только реальный
путь — симлинки не разворачивает и не «протаскивает».
### Следствие для архитектуры
```
Mac Контейнер-исполнитель
~/agents/ /agents (bind-mount, rw)
project-x/ ← РЕАЛЬНАЯ папка → видна
src -> ~/Developer/proj-x ← СИМЛИНК → ✗ БИТЫЙ
```
**Правило:** внутрь `~/agents` — только **реальные каталоги**. Симлинки не работают
на внешние цели, hardlink'ом каталог подключить нельзя (см. §2) → внешние проекты —
только **bind-mount**.
---
## 1a. 🔴 ПРОВЕРЕНО ЖИВЫМ ТЕСТОМ: live `mount --bind` на macOS Docker НЕ работает
Alex предложил: если bind на ходу возможен — делать и live, и в compose, чтобы контейнер
не пересоздавать. **Проверено тремя прогонами — на macOS невозможно.**
```
VM (nsenter -t 1 -m): /tmp/bindtest2/agents/existing/f.txt = "DATA2" ✓ файл есть
mount --bind ... → BIND_OK
контейнер (без рестарта): cat /agents/existing/f.txt
→ No such file or directory ✗ не видит
```
Проверено в трёх вариантах — результат одинаков:
1. Каталог создан **после** старта контейнера + live bind → в контейнере пусто.
2. Ожидание 5 с (гипотеза «кэш атрибутов virtiofs») → пусто.
3. Каталог существовал **до** старта контейнера + bind поверх него → **пусто**.
**Причина:** на macOS Docker Desktop отдаёт контейнеру файлы не через Linux `mount`,
а через **virtiofs** — отдельный демон, который строит маппинг путей **в момент старта
контейнера**. `mount`, сделанный внутри VM через `nsenter`, для virtiofs невидим:
изнутри VM всё корректно, до контейнера не доходит. Это не кэш — рестарт не поможет.
📌 **Следствие:** live-bind был бы возможен на Linux-хосте (там Docker нативный).
На macOS — **только через `-v` в compose + reproduce контейнера**.
`docker compose up -d` с новым `-v` = 2-3 сек, `--privileged` не нужен.
📌 **Тот же корень, что и с симлинками:** virtiofs-маппинг понимает только пути
**мира macOS**, известные ему на старте. Всё, что появляется в VM позже (`mount`,
шара, новый каталог) — в контейнер не проецируется.
### Сетевые шары (SMB/NFS) — тот же вывод
Смонтировать шару в macOS и дать `-v /Volumes/share:/data` **не сработает** (virtiofs
не видит `mount_smbfs`-точку). Единственный рабочий вариант — mount внутрь VM через
`nsenter`, но он не переживает рестарт Docker и требует `--privileged`.
**Рекомендация: не тянуть шару в контейнер.** Если данные на NAS — `rsync` в `~/agents`
по требованию вместо прямого монтирования.
---
## 2. Symlink / hardlink / bind-mount — что выбрать
| Способ | В Docker | Может пересечь ФС | Может указать на файл вне mount | Итог |
|---|---|---|---|---|
| **symlink** | ✗ битый | да | ✗ нет | ❌ не использовать для внешних целей |
| **hardlink** | ✓ работает | **✗ нет** | ✗ нет | ✓ для файлов внутри `~/agents` |
| **bind-mount** | ✓ работает | да | да | ✓ для внешних каталогов |
| **APFS clonefile** | ✓ (реальная копия) | да | да | ✓ для «копии проекта» без расхода диска |
### Ключевое ограничение hardlink
Hardlink **не может пересечь границу файловой системы** и **не может ссылаться на
каталог** (только на файл, и только на macOS — с `-F`). То есть замена симлинка
«проект целиком» через hardlink технически невозможна, если проект лежит
на другом томе или это директория.
**Вывод:** «линкование проекта извне» = **bind-mount**, а не линк. Линки — для
внутренней организации одного тома.
📌 **Прямо отвечая на исходный замысел Alex:** hardlink'ом **нельзя** подключить
проект-каталог извне — физически невозможно (каталоги не линкуются, ФС не пересекается).
Поэтому в целевой архитектуре внешние проекты висят на **bind-mount'ах**
(`~/Developer/<proj>:/agents/<name>`), а секция Dashboard управляет именно манифестом
`agents-mounts.yaml`. Hardlink-операции остаются как вспомогательный инструмент
для файлов внутри одного тома.
---
## 3. Целевая архитектура
```
Mattermost (Alex)
│ «почини баг X в project-y»
▼
Балда-оркестратор (Kraken, DeepSeek, mode=allow_all)
│ 1. читает Obsidian, формирует ТЗ
│ 2. /goalkeeper → work → validate
▼
Мост (Mac, FastAPI, :8790) ← новый сервис, регистрируется в Eagle Dashboard
│ HTTP + shared secret (WireGuard-хаб уже есть)
│ идемпотентность + таймаут + лимит 1-2 параллельных задач
▼
Контейнер-исполнитель (Mac, Docker)
│ -v ~/agents:/agents:rw ← рабочая область, локальный диск
│ -v ~/Developer/<proj>:/agents/<name> ← внешние проекты (bind, не линк)
│ -v ~/cli-auth/claude:/root/.claude:rw ← auth CLI на ХОСТЕ (вне /agents!)
│ -v ~/cli-auth/codex:/root/.codex:rw ← auth CLI на ХОСТЕ
│ Node 24 + codex + claude + git
│ CLAUDE_CODE_OAUTH_TOKEN из .env
▼
возврат: diff + вывод тестов + коммит → Балда → MM
```
### Данные CLI — на хосте, не в образе
🔴 Требование Alex: **данные codex/claude живут в хостовой папке**, чтобы не ушли
при пересборке образа, обновлении CLI или пересоздании контейнера.
**Расположение — `~/cli-auth`, ВНЕ `~/agents`** (решение принято 2026-09-26):
агент-исполнитель монтирует `/agents` на запись, и если auth лежит внутри, агент
получает доступ к своим же токенам. Вынос наружу закрывает это.
| Данные | Хост-путь | В контейнере | Права |
|---|---|---|---|
| Claude auth / сессии / конфиг | `~/cli-auth/claude` | `/root/.claude` | 0700 |
| Codex auth / конфиг | `~/cli-auth/codex` | `/root/.codex` | 0700 |
| OAuth-токен Claude | `~/Docker/balda-executor/.env` | env `CLAUDE_CODE_OAUTH_TOKEN` | 0600 |
| Ключ провайдера codex (если нужен) | `~/Docker/balda-executor/.env` | env | 0600 |
**Почему так:**
- пересборка образа `docker build` **не трогает** bind-mounted каталоги → auth переживает
всё; новый образ поднимается уже залогиненным;
- обновление версии CLI внутри образа не сбрасывает сессию;
- откат образа (`--force-recreate` на старый тег) не требует повторного логина;
- токен не попадает в слои образа → не утечёт при `docker save`/шаринге;
- агент **не видит** свои токены (они вне смонтированного `/agents`).
⚠️ `~/cli-auth` обязан быть на **том же томе**, что и корень Docker-маппинга, иначе
проект упрётся в virtiofs-ограничения (см. §1a).
---
## 4. Секция «Agents» в Eagle Dashboard
### Где живёт код (проверено 2026-09-26)
| Файл | Строк | Что править |
|---|---|---|
| `~/Developer/eagle-dash/main.py` | 889 | новые `@app.get/post/delete` роуты |
| `~/Developer/eagle-dash/templates/index.html` | 1248 | новая вкладка + Alpine.js стейт |
| `~/Developer/eagle-dash/health.py` | 181 | health-чек моста (`type: http`) |
| `~/Developer/eagle-dash/services.yaml` | 6.5 КБ | регистрация сервиса `agents-bridge` |
**Существующие вкладки:** `services` · `crons` · `pages` · `files`
(роутинг через `?tab=`, `setUrlTab()`, `activeTab` в Alpine).
### Что делает новая секция
1. **Список содержимого `~/agents`** — по каждому проекту: имя, тип (реальная папка /
mount / битый symlink), размер, дата изменения, **статус**.
2. **Создать проект** — `mkdir ~/agents/<name>`.
3. **Подключить внешний проект** — запись в `~/Developer/agents-mounts.yaml`
(`name` + `host_path`) → `docker compose up -d` (**2-3 сек, без privileged**).
⚠️ **Только bind-mount.** Ни symlink (битый), ни hardlink (каталоги не линкуются).
⚠️ Live `mount --bind` **недоступен на macOS** (см. §1a) — recreate обязателен.
4. **Hardlink-операции** — `ln -F <file> ~/agents/<proj>/<name>` (только **файлы**,
только внутри одного тома; в UI показывать `st_dev` и блокировать при разных ФС).
5. **Удалить из контейнера** — убрать из `agents-mounts.yaml` + `docker compose up -d`
⚠️ **Файлы на диске не трогать** — только отвязка от контейнера (см. Core Rules).
6. **Открыть терминал** — ссылка на существующую вкладку `files`.
### API-контракт (черновик)
```
GET /api/agents → [{name, kind, size, mtime, link_valid, target}]
POST /api/agents {name} → mkdir ~/agents/<name>
POST /api/agents/{name}/mount {host_path} → добавить bind-mount + recreate
DELETE /api/agents/{name}/mount → убрать bind-mount + recreate (файлы целы)
POST /api/agents/{name}/hardlink {file, name} → ln -F (с проверкой st_dev)
GET /api/agents/{name}/usage → размер, место на томе
```
### Чего секция НЕ делает
- **Не удаляет** содержимое `~/agents` — только отвязывает от контейнера.
- **Не убивает** процессы — lifecycle контейнера через существующий supervisor.
- **Не создаёт symlink** на внешние цели — вместо них bind-mount.
---
## 5. Система подтверждений — итог разведки
### Как устроено в Balda (апстрим, `docs/architecture/interactive-questions.md`)
Три режима (`permissioncmd.Mode`):
| Режим | Поведение |
|---|---|
| `allow_all` | никого не спрашивает — **выбран Alex** |
| `ask` | ADK-callback **блокирует турн**, вопрос уходит в чат, ждёт ответа, таймаут fail-closed, продолжает через `ResumeTarget` |
| `deny_all` | всё запрещено |
🔴 **`ask` сейчас работает только для Telegram и Slackagent** — прямая цитата
`interactive-questions.md`: *«`ask` is supported for Telegram and Slackagent and
fails closed elsewhere»*. Для Mattermost `ask` **фейлится закрыто**.
### Что это значит для сценария «MCP просит линк → Alex подтверждает из чата»
Прямой путь **сейчас невозможен** поверх MM. Варианты:
| Вариант | Объём | Ложится куда |
|---|---|---|
| A. Дописать `ask` в MM | ~1-2 дня | ветка `feat/mattermost-channel` → **расширение PR #8** |
| B. Апрув в мосте (свой мини-эндпоинт + очередь) | ~0.5 дня | FastAPI-мост Фазы 4 |
**С Alex выбран `allow_all`** — этим заходом подтверждения не нужны. Пункт
оставлен как задел: когда `ask` понадобится, начинать с варианта **B** (дешевле,
не зависит от ревью апстрима).
---
## 6. План работ (по шагам)
### Шаг 1 — `~/agents` + `~/cli-auth` + контейнер-исполнитель (~2 ч)
1. `mkdir ~/agents`, права `admin:staff`; `~/cli-auth/{claude,codex}` (0700).
2. `~/Docker/balda-executor/Dockerfile` — Node 24 + `codex` + `claude` + `git`.
3. `~/Docker/balda-executor/docker-compose.yml`:
```yaml
volumes:
- /Users/admin/agents:/agents:rw
- /Users/admin/cli-auth/claude:/root/.claude:rw # auth на хосте
- /Users/admin/cli-auth/codex:/root/.codex:rw # auth на хосте
```
+ `agents-mounts.yaml` подключается генератором override-файла
(`compose.override.yaml`), чтобы composefile не правился скриптом вручную.
4. Авторизация один раз: `claude setup-token` на Mac → токен в
`~/Docker/balda-executor/.env` (0600); `codex` — свой flow в `~/cli-auth/codex`.
5. Проверка: смонтированные auth видны (`docker exec ... ls /root/.claude`),
`claude --version` + `codex --version` работают, повторный `up -d` остаётся залогинен.
### Шаг 2 — Мост на Mac (~3-4 ч)
1. `~/Developer/agents-bridge/` — FastAPI, порт **8790**.
2. Эндпоинты: `POST /task` (idempotency key), `GET /task/{id}`, `GET /healthz`.
3. Запуск CLI headless в контейнере, возврат diff + тестов.
4. Лимит параллельных задач: **1-2** (риск «unusual traffic» — см. §Риски плана Фаз).
5. Shared secret в заголовке + проверка source IP (WireGuard-подсеть).
### Шаг 3 — Сеть Kraken → Mac (~1 ч)
1. WireGuard-туннель (хаб на VPS уже есть — см. скилл `wireguard-vps-hub`).
2. Проверка: `curl` с Kraken на Mac `:8790/healthz`.
### Шаг 4 — Секция «Agents» в Eagle Dashboard (~3-4 ч)
1. `main.py` — 6 роутов из §4.
2. `templates/index.html` — вкладка `agents` + `?tab=agents`.
3. `services.yaml` — запись `agents-bridge` (`health.type: http`, url `:8790/healthz`).
4. `__pycache__` + `launchctl kickstart -k gui/$(id -u)/com.eagle.dashboard`.
### Шаг 5 — Связка Балда → мост (~2 ч)
1. MCP-инструмент в Балде (или webhook) → `POST /task`.
2. Проверка на задаче-пустышке: `echo ok > /agents/test/probe.txt`.
### Шаг 6 — Приёмка Фазы 5 (~1 день)
1. Сквозной тест **от аккаунта Alex**: `@balda почини X` → diff в MM.
2. Расходы: DeepSeek-токены + подписочные лимиты.
3. Задокументировать: как работает, где логи, как откатить.
---
## 7. Риски
| Риск | Оценка | Митигация |
|---|---|---|
| Симлинк в `~/agents` на внешний путь → битый | **подтверждён** | bind-mount вместо линка; UI помечает битые линки |
| Live `mount --bind` на macOS → контейнер не видит | **подтверждён** | только `-v` + `compose up -d`; live-bind вычеркнут из плана |
| Сетевую шару (SMB/NFS) в контейнер протянуть нельзя | **подтверждён** | не тянуть: `rsync` в `~/agents` по требованию |
| Hardlink через границу ФС → `EXDEV` | высокий | проверять `st_dev` перед `ln`; UI блокирует |
| Бан подписочного аккаунта за «unusual traffic» | средний | 1-2 параллельных задачи, официальный бинарь, без прокси токена |
| Полный доступ (`allow_all`) + MCP → агент может снести `~/agents` | **средний** | бэкапы + `git rm`-семантика вместо `rm`; правки только через bind-mount-манифест |
| Агент читает/пишет свои же auth-данные | средний | `~/cli-auth` **вне** `~/agents` → агент их не видит |
| Мост открыт наружу | высокий | только WireGuard-подсеть + shared secret; не публиковать порт в LAN |
---
## 8. Открыто (требует ответа Alex при старте)
1. Порт моста — **8790** (взят свободный; проверить `lsof -i :8790` перед стартом).
2. Провайдер исполнителя — **claude** (подписка уже залогинена) или **codex**?
3. Нужен ли `ask`-апрув в будущем, или `allow_all` достаточно постоянно?
4. `~/cli-auth` (auth вне `~/agents`, агент не видит) vs `.cli-auth` внутри — по умолчанию **вне**.
## История
- **2026-09-26**: создан. Решения Alex: папка `~/agents`, full access, управление
линками — секция в Eagle Dashboard. Живым тестом доказано, что симлинк в Docker
не читается → внешние проекты подключать bind-mount'ом, не линком.
- **2026-09-26 (продолжение)**: два уточнения доказаны живыми тестами.
**Live `mount --bind` на macOS Docker НЕ работает** (virtiofs видит VM-mount'ы только
на старте контейнера) → recreate обязателен, live-bind вычеркнут (3 прогона, все
отрицательные). **Сетевые шары** — тот же корень, в контейнер не протянуть.
Добавлен раздел про **данные CLI на хосте** (`~/cli-auth` вне `~/agents`) по
требованию Alex: auth переживает пересборку образа и обновление CLI.
## Связанные заметки
- [[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, 8/8 CI, ждёт ревью)
- [[personal/tech/balda-go-fx-pitfalls]] — fx-питфоллы, дельта-апплаер
- скилл `eagle-dashboard` — архитектура дашборда, health-типы, launchd-нюансы
- скилл `wireguard-vps-hub` — туннель Kraken → Mac