# Фаза 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/:/agents/`), а секция 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/:/agents/ ← внешние проекты (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/`. 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 ~/agents//` (только **файлы**, только внутри одного тома; в 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/ 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