22 KiB
Фаза 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 ✗ не видит
Проверено в трёх вариантах — результат одинаков:
- Каталог создан после старта контейнера + live bind → в контейнере пусто.
- Ожидание 5 с (гипотеза «кэш атрибутов virtiofs») → пусто.
- Каталог существовал до старта контейнера + 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).
Что делает новая секция
- Список содержимого
~/agents— по каждому проекту: имя, тип (реальная папка / mount / битый symlink), размер, дата изменения, статус. - Создать проект —
mkdir ~/agents/<name>. - Подключить внешний проект — запись в
~/Developer/agents-mounts.yaml(name+host_path) →docker compose up -d(2-3 сек, без privileged). ⚠️ Только bind-mount. Ни symlink (битый), ни hardlink (каталоги не линкуются). ⚠️ Livemount --bindнедоступен на macOS (см. §1a) — recreate обязателен. - Hardlink-операции —
ln -F <file> ~/agents/<proj>/<name>(только файлы, только внутри одного тома; в UI показыватьst_devи блокировать при разных ФС). - Удалить из контейнера — убрать из
agents-mounts.yaml+docker compose up -d⚠️ Файлы на диске не трогать — только отвязка от контейнера (см. Core Rules). - Открыть терминал — ссылка на существующую вкладку
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 ч)
mkdir ~/agents, праваadmin:staff;~/cli-auth/{claude,codex}(0700).~/Docker/balda-executor/Dockerfile— Node 24 +codex+claude+git.~/Docker/balda-executor/docker-compose.yml: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 не правился скриптом вручную.
- Авторизация один раз:
claude setup-tokenна Mac → токен в~/Docker/balda-executor/.env(0600);codex— свой flow в~/cli-auth/codex. - Проверка: смонтированные auth видны (
docker exec ... ls /root/.claude),claude --version+codex --versionработают, повторныйup -dостаётся залогинен.
Шаг 2 — Мост на Mac (~3-4 ч)
~/Developer/agents-bridge/— FastAPI, порт 8790.- Эндпоинты:
POST /task(idempotency key),GET /task/{id},GET /healthz. - Запуск CLI headless в контейнере, возврат diff + тестов.
- Лимит параллельных задач: 1-2 (риск «unusual traffic» — см. §Риски плана Фаз).
- Shared secret в заголовке + проверка source IP (WireGuard-подсеть).
Шаг 3 — Сеть Kraken → Mac (~1 ч)
- WireGuard-туннель (хаб на VPS уже есть — см. скилл
wireguard-vps-hub). - Проверка:
curlс Kraken на Mac:8790/healthz.
Шаг 4 — Секция «Agents» в Eagle Dashboard (~3-4 ч)
main.py— 6 роутов из §4.templates/index.html— вкладкаagents+?tab=agents.services.yaml— записьagents-bridge(health.type: http, url:8790/healthz).__pycache__+launchctl kickstart -k gui/$(id -u)/com.eagle.dashboard.
Шаг 5 — Связка Балда → мост (~2 ч)
- MCP-инструмент в Балде (или webhook) →
POST /task. - Проверка на задаче-пустышке:
echo ok > /agents/test/probe.txt.
Шаг 6 — Приёмка Фазы 5 (~1 день)
- Сквозной тест от аккаунта Alex:
@balda почини X→ diff в MM. - Расходы: DeepSeek-токены + подписочные лимиты.
- Задокументировать: как работает, где логи, как откатить.
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 при старте)
- Порт моста — 8790 (взят свободный; проверить
lsof -i :8790перед стартом). - Провайдер исполнителя — claude (подписка уже залогинена) или codex?
- Нужен ли
ask-апрув в будущем, илиallow_allдостаточно постоянно? ~/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