# Секция «Agents» в Eagle Dashboard — реализация > Создан 2026-09-26. Шаг 4 Фазы 4 [[personal/plans/balda-mm-phase4-agents]]. > Статус: **код написан, НЕ активирован** — дашборд работает на старом байткоде. > Активация = `__pycache__` + `launchctl kickstart -k` (апрув не получен). --- ## Расположение | Что | Путь | |---|---| | Роутер | `~/Developer/eagle-dash/agents.py` (новый файл) | | Подключение | `~/Developer/eagle-dash/main.py` → `app.include_router(agents_router)` | | Реестр сервисов | `~/Developer/eagle-dash/services.yaml` → запись `agents_bridge` | | Бэкап реестра | `services.yaml.bak-agents-20260926-185641` | | Манифест монтирований | `~/Docker/balda-executor/agents-mounts.yaml` | | Генератор override | `~/Docker/balda-executor/tools/render-mounts.py` | --- ## API (`prefix=/api/agents`) ``` GET /api/agents → список ~/agents + манифест + статус контейнера POST /api/agents {name} → mkdir ~/agents/ POST /api/agents/{name}/mount {host_path, mode} → запись в манифест + recreate DELETE /api/agents/{name}/mount → отвязать (файлы на диске целы) POST /api/agents/{name}/hardlink {file, link_name} → ln -F с проверкой st_dev GET /api/agents/{name}/usage → du -sk GET /api/agents/mounts → содержимое манифеста POST /api/agents/mounts/render → перегенерировать override без recreate GET /api/agents/bridge → состояние моста :8790 + `launchctl list` ``` ## Что секция НЕ делает (осознанно) | Запрещено | Почему | |---|---| | Удалять файлы в `~/agents` | правило Alex: только отвязка от контейнера | | Создавать symlink на внешний путь | в Docker битый (virtiofs) — см. [[personal/tech/macos-docker-virtiofs-mount-limits]] | | Hardlink на каталог | ядро запрещает; вернётся ошибка с объяснением | | Hardlink через границу ФС | `st_dev` сравнивается до `ln`, при различии — 400 с текстом про `EXDEV` | | Живой `mount --bind` | на macOS не виден контейнеру — только `docker-compose up -d` | --- ## Ключевые решения реализации ### Роутер в отдельном файле, не в `main.py` `main.py` — 889 строк; секция «Agents» вынесена в `agents.py` (~430 строк) как `APIRouter(prefix="/api/agents")`. Подключение одной строкой в `main.py` — минимизирует правку большого файла и не ломает существующие роуты. ### Валидация `host_path` — белый список запретов Путь должен быть абсолютным, существующим каталогом, и НЕ одним из: `/`, `/Users`, `/Users/admin`, `/Users/admin/agents`. Имя агента — `^[a-z0-9][a-z0-9._-]{0,62}$`. Это защита от монтирования `~` целиком в контейнер. ### Откат манифеста при провале `mount`/`unmount` пишут манифест → вызывают `render-mounts.py` → `docker-compose up -d`. Если любой шаг падает, **манифест возвращается к прежнему состоянию** (`_write_manifest(mounts)`), чтобы файл не расходился с реально смонтированным. ### Запись манифеста — атомарная `tmp` → `replace()`. Заголовок с правилами перезаписывается константой `MANIFEST_HEADER`, чтобы пояснения не терялись при правках из UI. ### `du -sk` вместо обхода дерева Список агентов считает размер через `rglob` (для UI), а `/{name}/usage` — через `du -sk`. Выбор `du` для usage: не блокирует event loop на больших каталогах дольше необходимого и не следует по монтированиям вглубь без нужды. --- ## Питфоллы ### 1. Дашборд кеширует байткод Правки `.py` не видны до перезапуска. Порядок: ```bash rm -rf ~/Developer/eagle-dash/__pycache__ launchctl kickstart -k gui/$(id -u)/com.eagle.dashboard ``` `kickstart` **без** `-k` переиспользует процесс и новый код не подхватывает. ### 2. `docker compose` vs `docker-compose` На этой машине compose — отдельный бинарь `/opt/homebrew/bin/docker-compose` (v5.1.3). `docker compose` → `docker: unknown command`. В `agents.py` все вызовы через `docker-compose`. ### 3. launchd не читает shell `PATH` Дашборд под launchd имеет `PATH=/usr/bin:/bin`. Поэтому: - `lsof` вызывается полным путём `/usr/sbin/lsof`; - `agents.py` из `PATH` дёргает `docker`, `docker-compose`, `du`, `launchctl` — на `/usr/bin` и `/bin` из них попадают только `du` и `launchctl`; `docker`/`docker-compose` из `/opt/homebrew/bin` **под launchd не найдутся**. ⚠️ **Не проверено живьём** (дашборд не перезапущен). Если mount/unmount будут возвращать 500 с текстом «команда не найдена» — заменить в `agents.py` литералы на абсолютные пути `/opt/homebrew/bin/docker-compose` и `/opt/homebrew/bin/docker`. ### 4. `services.yaml` — запись `agents_bridge` Добавлена в начало секции `services:` (файл отсортирован по `id`, `agents_bridge` первый): ```yaml agents_bridge: access: localhost category: orchestration critical: false health: type: http url: http://127.0.0.1:8790/healthz icon: 🌉 label: com.agents.bridge log: /Users/admin/Library/Logs/agents-bridge.err.log name: Agents Bridge port: 8790 type: daemon working_dir: /Users/admin/Developer/agents-bridge ``` ⚠️ `type: daemon` заставит **supervisor дашборда** взять сервис под управление и запустить свой процесс — а мост уже управляется собственным launchd (`com.agents.bridge`). Возможен конфликт за порт 8790. Ревизия при активации: либо `type: system` + `health.type: launchctl` (label `com.agents.bridge`), либо `type: none`. `health.type: http` на `:8790/healthz` работает в любом варианте. --- ## Активация (одна команда) ```bash rm -rf ~/Developer/eagle-dash/__pycache__ && \ launchctl kickstart -k gui/$(id -u)/com.eagle.dashboard ``` Затем: ```bash curl -s http://127.0.0.1:8880/api/agents | /usr/bin/jq open "http://127.0.0.1:8880/?tab=agents" ``` --- ## Связанное - [[personal/plans/balda-mm-phase4-agents]] — план Фазы 4 - [[personal/tech/agents-bridge]] — мост :8790 - [[personal/tech/macos-docker-virtiofs-mount-limits]] — ограничения macOS Docker - скилл `eagle-dashboard` — архитектура дашборда, вкладки, кеш байткода