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

22 KiB
Raw Blame History

Фаза 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 по требованию вместо прямого монтирования.


Способ В Docker Может пересечь ФС Может указать на файл вне mount Итог
symlink ✗ битый да ✗ нет ❌ не использовать для внешних целей
hardlink ✓ работает ✗ нет ✗ нет ✓ для файлов внутри ~/agents
bind-mount ✓ работает да да ✓ для внешних каталогов
APFS clonefile ✓ (реальная копия) да да ✓ для «копии проекта» без расхода диска

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:
    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.

Связанные заметки