# Фаза 4 — исполнитель на Mac + `~/agents` + секция «Agents» в Eagle Dashboard > План. **Шаги 1-3 выполнены** (2026-09-26): образ собран, контейнер `balda-executor` Up (healthy), > `claude 2.1.283` + `codex 0.157.1` внутри, монтирования проверены в обе стороны. > Мост `agents-bridge` :8790 — под **launchd** (`com.agents.bridge`, `RunAtLoad` + `KeepAlive`), > протестирован (auth 401, идемпотентность, сквозной вызов CLI). > Шаг 4 — **код написан, не активирован** (нужен перезапуск дашборда). > 🔴 Осталось: **логин CLI делает Alex сам**; активация секции «Agents»; шаг 3 (туннель); > шаги 5-6. > Часть [[personal/projects/balda/balda-mm-delegation-plan]] (Фазы 4-5). > Предыдущее: [[personal/projects/balda/balda-kraken-mm-deploy]] (Фаза 3 ✅ — бот отвечает Alex). --- ## 1. Решения | # | Вопрос | Решение | |---|---|---| | 1 | Папка для проектов исполнителя | `~/agents` | | 2 | Подтверждения | Full access → `permissions.mode: allow_all` | | 3 | Управление линками в `~/agents` | Новая секция «Agents» в Eagle Dashboard | | 4 | Auth CLI (claude/codex) | на хосте, `~/cli-auth`, **вне** `~/agents` | | 5 | Сеть Kraken → Mac | Cloudflare Tunnel (существующий cloudflared на Mac) | | 6 | Провайдер исполнителя (открытый п.1) | `claude` — оба CLI стоят в образе, переключение через `EXECUTOR_PROVIDER` в `.env` | --- ## 2. Ограничения macOS Docker (проверено тестами) | Ограничение | Итог | |---|---| | Симлинк на внешний путь | битый в контейнере — цель резолвится от корня контейнера | | Live `mount --bind` внутри VM | контейнер не видит (virtiofs строит маппинг на старте) | | Сетевая шара SMB/NFS | в контейнер не протянуть (тот же virtiofs) | | Hardlink на каталог | физически невозможен (ядро запрещает) | | Hardlink через границу ФС | `EXDEV` | **Следствие:** внешние проекты подключаются **только bind-mount'ом через `-v`** + `docker compose up -d` (2-3 сек). Hardlink — только для файлов внутри одного тома. Детали механизма: [[personal/tech/macos-docker-virtiofs-mount-limits]]. --- ## 3. Архитектура ``` Mattermost (Alex) │ «почини баг X в project-y» ▼ Балда-оркестратор (Kraken, DeepSeek, allow_all) │ читает Obsidian, формирует ТЗ, /goalkeeper → work → validate ▼ Cloudflare Tunnel: agents-bridge.qentra.top → localhost:8790 │ (существующий cloudflared на Mac, PID + LaunchAgent) ▼ Мост (Mac, FastAPI :8790) │ idempotency key, таймаут, лимит 1-2 параллельных задач, shared secret ▼ Контейнер-исполнитель (Mac, Docker) │ -v ~/agents:/agents:rw │ -v ~/Developer/:/agents/:rw ← внешние проекты │ -v ~/cli-auth/claude:/root/.claude:rw ← auth на хосте │ -v ~/cli-auth/codex:/root/.codex:rw ← auth на хосте │ Node 24 + codex + claude + git ▼ diff + тесты + коммит → Балда → Mattermost ``` ### Auth CLI на хосте | Данные | Хост | В контейнере | Права | |---|---|---|---| | Claude auth | `~/cli-auth/claude` | `/root/.claude` | 0700 | | Codex auth | `~/cli-auth/codex` | `/root/.codex` | 0700 | | OAuth-токен Claude | `~/Docker/balda-executor/.env` | env | 0600 | 📌 **Авторизацию CLI делает Alex сам** (решение 2026-09-26). Копирование auth-файлов из `~/.claude`/`~/.codex` в `~/cli-auth` **отклонено** — я эти файлы не читаю и не копирую. До его логина любая задача вернёт `Not logged in · Please run /login` (это ответ Claude Code, а не ошибка моста — конвейер при этом доказанно рабочий). Токен `CLAUDE_CODE_OAUTH_TOKEN` в `.env` контейнера — запасной путь для headless без монтирования. Пересборка образа и обновление CLI auth не сбрасывают. Токены вне `/agents` → агент их не видит. --- ## 4. Секция «Agents» в Eagle Dashboard Правки: `main.py` (роуты) · `templates/index.html` (вкладка `?tab=agents`) · `services.yaml` (запись `agents-bridge`, `health.type: http`) · `health.py`. | Функция | Что делает | |---|---| | Список | содержимое `~/agents`: имя, тип, размер, mtime, статус линка | | Создать | `mkdir ~/agents/` | | Подключить внешний | запись в `agents-mounts.yaml` → `compose up -d` | | Hardlink | `ln -F` для файлов, проверка `st_dev` | | Отключить | убрать из манифеста + `up -d`; **файлы на диске целы** | | Терминал | ссылка на вкладку `files` | **API:** ``` GET /api/agents → список POST /api/agents {name} → mkdir POST /api/agents/{name}/mount {host_path} → bind + recreate DELETE /api/agents/{name}/mount → отвязать (файлы целы) POST /api/agents/{name}/hardlink {file, name} → ln -F со st_dev GET /api/agents/{name}/usage → размер ``` **Не делает:** не удаляет файлы, не убивает процессы, не создаёт symlink на внешние цели. --- ## 5. Подтверждения Режимы Balda: `allow_all` (выбран) · `ask` · `deny_all`. `ask` поддержан **только для Telegram и Slackagent**; для Mattermost фейлится закрыто. Сценарий «подтвердить из чата» требует: **A** — дописать `ask` в MM (~1-2 дня, расширение PR #8), либо **B** — апрув в мосте (~0.5 дня). При `allow_all` не нужно; начинать с B, когда понадобится. --- ## 6. Шаги ### ✅ Шаг 1 ВЫПОЛНЕН (2026-09-26) Артефакты на диске (все сверены и рабочие; черновики от 15:36 доведены до состояния «собрано и запущено»): | Путь | Что | Состояние | |---|---|---| | `~/agents` | рабочая область (`admin:staff`) | ✅ монтируется в `/agents`, запись в обе стороны проверена | | `~/cli-auth/{claude,codex}` | auth CLI, 0700, **вне** `~/agents` | ✅ монтируются в `/root/.claude`, `/root/.codex` | | `~/Docker/balda-executor/Dockerfile` | `node:24-bookworm` + claude + codex + git + `tini` | ✅ исправлен (см. ниже) | | `~/Docker/balda-executor/docker-compose.yml` | 3 тома, сеть `executor-net`, `build:` | ✅ исправлен (см. ниже) | | `~/Docker/balda-executor/agents-mounts.yaml` | манифест внешних проектов (пустой список) | ✅ создан | | `~/Docker/balda-executor/tools/render-mounts.py` | генератор `compose.override.yaml` | ✅ создан, `--check` работает | | `~/Docker/balda-executor/compose.override.yaml` | **генерируется**, руками не править | ✅ сгенерирован | | `~/Docker/balda-executor/.env` | 0600, `EXECUTOR_PROVIDER=claude` | ✅ создан (без секретов) | **Что уже работает (проверено):** ``` balda-executor Up (healthy) balda-executor:0.1.0 claude → 2.1.283 (Claude Code) codex → codex-cli 0.157.1 ``` `~/agents` ↔ `/agents` пишется в обе стороны; рестарт контейнера состояние сохраняет; auth-каталоги хоста видны внутри. #### 🔴 Три исправленных дефекта черновика (не повторять) | Дефект | Симптом | Фикс | |---|---|---| | `env_file: .env` без файла | `docker compose up` падает на старте | `.env` создаётся **первым** шагом, 0600 | | Нет `compose.override.yaml` | `up` падает на монтировании несуществующего пути | `tools/render-mounts.py` запускается **до** `up` | | `FROM node:24-bookworm` без явных CA | — | `ca-certificates` + `update-ca-certificates` в Dockerfile (условие работы HTTPS у CLI) | #### ⚠️ На macOS этой машины — `docker-compose`, НЕ `docker compose` Docker CLI 29.4.1 + buildx как плагин (`~/.docker/cli-plugins/`), а compose — **отдельный бинарь** `/opt/homebrew/bin/docker-compose` (v5.1.3) с симлинком в `/opt/homebrew/lib/docker/cli-plugins/`. `docker compose` → `docker: unknown command: docker compose`; `docker compose config --quiet` → `unknown flag: --quiet`. 📌 **Все команды в этом проекте — через `docker-compose`**, включая `docker-compose config` (без `--quiet`). #### Сборка образа (сработавшая команда) ```bash cd ~/Docker/balda-executor python3 tools/render-mounts.py --check # 0 записей, ошибок нет python3 tools/render-mounts.py # → compose.override.yaml docker-compose config --services # → executor (валидация) docker buildx build --builder arm64builder --platform linux/arm64 \ --provenance=false --load -t balda-executor:0.1.0 . docker-compose up -d docker-compose restart # проверка переживания рестарта ``` ⚠️ Билдер — `arm64builder` (colima, `docker-container` driver). Дефолтный билдер (`default` → `/var/run/docker.sock`) в статусе `error` — не использовать. ⚠️ `deploy.resources.limits.memory: 8G` из черновика **убран**: в не-swarm compose эта секция молча игнорируется, лимит создавал ложное ощущение защиты. #### ✅ Логин CLI — зона Alex (не автоматизируется) `~/cli-auth/claude` пуст, в `~/cli-auth/codex` только `tmp/`. 🔴 Копирование auth-файлов **отклонено Alex** («авторизую я, копировать из кредсы не надо»). Не предлагать повторно, не читать `~/.claude/.credentials.json`, `~/.codex/auth.json`. Признак незалогиненного исполнителя в ответе задачи: `Not logged in · Please run /login`. ### ✅ Шаг 2 ВЫПОЛНЕН (2026-09-26) — мост `agents-bridge` Проект: `~/Developer/agents-bridge/` (FastAPI, python из `~/Developer/eagle-dash/.venv`, py3.14). | Файл | Роль | |---|---| | `app.py` | роуты, авторизация, лимит параллельности, ThreadPoolExecutor | | `store.py` | SQLite WAL: `tasks` + UNIQUE-индекс по `idempotency_key` | | `executor.py` | `docker exec` в контейнер, провайдеры claude/codex, таймаут, обрезка вывода | | `config.py` | чтение `.env` (минимальный парсер, без python-dotenv на старте) | | `.env` | 0600, `BRIDGE_SECRET` (64 симв.), **не коммитить** | | `.env.example` | шаблон без секретов | | `state.sqlite3` | статусы задач, WAL | **API:** ``` GET /healthz → статус моста + контейнера (без секрета) POST /task → 202 {task_id, status} | 200 (идемпотентный повтор) | 401 | 429 | 400 GET /task/{task_id} → статус + stdout/stderr/exit_code GET /tasks?limit=N → последние задачи ``` **Авторизация:** заголовок `X-Bridge-Secret` на всех `/task*`. Идемпотентность — заголовок `Idempotency-Key`; повтор возвращает **тот же** `task_id` со HTTP **200** (первый ответ — 202). **Проверено живьём (2026-09-26):** | Проверка | Результат | |---|---| | `POST /task` без секрета / с неверным | 401 / 401 ✅ | | Два POST с одним `Idempotency-Key` | один `task_id` ✅ | | Задача дошла до CLI в контейнере | ответ CLI вернулся ✅ | | Статус в SQLite (WAL) | ✅ | | `/healthz` видит контейнер | `container_running: true` ✅ | Ответ `/healthz`: ```json {"status":"ok","container":"balda-executor","container_running":true, "provider":"claude","secret_configured":true,"active_tasks":0,"max_concurrent":1} ``` Результат сквозной пробы (до логина CLI): `status=failed, exit_code=1, stdout='Not logged in · Please run /login'` — **это ожидаемо** и доказывает, что путь Kraken → Mac → контейнер → CLI проходит целиком. **Запуск (вручную, до launchd):** ```bash cd ~/Developer/agents-bridge ~/Developer/eagle-dash/.venv/bin/python -m uvicorn app:app --host 127.0.0.1 --port 8790 ``` ### ✅ Шаг 3 ВЫПОЛНЕН (2026-09-26) — мост под launchd Плейт `com.agents.bridge`: `~/Library/LaunchAgents/com.agents.bridge.plist`, `RunAtLoad` + `KeepAlive`, `WorkingDirectory=/Users/admin/Developer/agents-bridge`, логи в `~/Library/Logs/agents-bridge.{log,err.log}`. Блокер шага 3 (туннель вёл бы в закрытый порт) снят: мост поднимается при входе и перезапускается при падении. ```bash launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.agents.bridge.plist launchctl list | grep agents.bridge # → 3063 0 com.agents.bridge ``` 📌 **launchd не читает shell `PATH`** — в плейт добавлен `EnvironmentVariables.PATH` с `/opt/homebrew/bin` (иначе `executor.py` не найдёт `docker`). ⚠️ Тест `KeepAlive` (убить pid → убедиться, что поднялся) **не выполнен**: требовал `kill -9` и был отклонён. Штатная проверка — `launchctl kickstart -k`. #### 🔴 Питфолл: `BRIDGE_SECRET` и redaction Hermes Команда, создающая `.env` через `python -c` с f-строкой `BRIDGE_SECRET={...}`, в логе выглядит обрезанной (`E...cret written`) — **это редакция секретов Hermes, файл на диске цел**. Проверять содержимое не чтением файла, а через awk с маскировкой: ```bash /usr/bin/awk -F= '{ if ($1 ~ /SECRET|TOKEN|KEY|PASSWORD/) print $1"="; else print }' .env ``` `read_file` на `.env` отказывает по определению (secret-bearing file). **3. Сеть (~1 ч)** — Cloudflare Tunnel Добавить published route `agents-bridge.qentra.top` → HTTP `localhost:8790` + Access Application. Токен и процесс cloudflared не трогаются. Проверка: `curl` извне на `/healthz`. ⚠️ hostname — **одноуровневый** (двухуровневые дают TLS handshake failure). 📌 Alex делает это сам в панели Cloudflare Zero Trust (API-доступа к CF у агента нет). Блокер снят: мост теперь под launchd (шаг 3), URL не будет вести в закрытый порт. **4. Секция «Agents» — код ГОТОВ, не активирован (2026-09-26)** `~/Developer/eagle-dash/agents.py` (роутер, 8 эндпоинтов) + `main.py` (`app.include_router`) + `services.yaml` (запись `agents_bridge`). Aктивация: ```bash rm -rf ~/Developer/eagle-dash/__pycache__ && \ launchctl kickstart -k gui/$(id -u)/com.eagle.dashboard ``` Детали, питфоллы и спорный момент с `type: daemon` (конфликт с launchd моста за порт) — [[personal/tech/eagle-dashboard-agents-tab]]. Вкладка `?tab=agents` в шаблоне **ещё не сделана**. **5. Связка Балда → мост (~2 ч)** MCP-инструмент в Балде → `POST /task`; проба `echo ok > /agents/test/probe.txt`. **6. Приёмка (~1 день)** Сквозной тест **от аккаунта Alex** в MM: `@balda` → задача → diff в ответе. Замер расходов (DeepSeek + подписка), документация отката. --- ## 7. Риски | Риск | Оценка | Митигация | |---|---|---| | Мост опубликован в интернет | высокий | Access Application + shared secret | | Полный доступ агента к `~/agents` | средний | бэкапы; `git rm`-семантика вместо `rm` | | Бан подписки за «unusual traffic» | средний | 1-2 параллельных задачи, официальный бинарь, без прокси токена | | Hardlink через ФС → `EXDEV` | высокий | проверка `st_dev` в UI | | Mac спит → туннель и мост недоступны | средний | задачи в durable-очереди Балды, идемпотентность | --- ## 8. Открыто 1. ~~Провайдер исполнителя~~ → **claude** (оба в образе, переключение `EXECUTOR_PROVIDER`) 2. ~~Нужен ли `ask`-апрув~~ → при `allow_all` не нужен; при потребности начинать с B (апрув в мосте, ~0.5 дня) 3. 🔴 **Логин CLI — делает Alex сам.** Копирование auth-файлов отклонено; до логина задачи возвращают `Not logged in` 4. ~~Мост не в launchd~~ → ✅ **закрыто**: `com.agents.bridge` (`RunAtLoad` + `KeepAlive`) 5. 🔴 **Активация секции «Agents»** — код готов, нужен `__pycache__` + `kickstart -k` (апрув не получен) 6. ⚠️ **Конфликт за порт 8790** — `agents_bridge` в `services.yaml` объявлен `type: daemon`, но мост уже управляется своим launchd. Ревизия при активации: `type: system` + `health.type: launchctl` либо `type: none`. См. [[personal/tech/eagle-dashboard-agents-tab]] §4 7. ⚠️ Вкладка `?tab=agents` в `templates/index.html` не сделана — API готов, UI нет 8. Hostname моста — `agents-bridge.qentra.top` (шаг 3, public route добавляет Alex в CF) 9. Порт `8790` — свободен до старта моста, занят после; иных слушателей нет 10. ⚠️ Под launchd `agents.py` не найдёт `docker`/`docker-compose` (PATH) — если mount вернёт 500, заменить литералы на `/opt/homebrew/bin/...`. Живьём не проверено (дашборд не перезапущен) ## Связанные заметки - [[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) - [[personal/tech/agents-bridge]] — мост :8790, API, питфоллы (шаг 2 ✅) - [[personal/tech/eagle-dashboard-agents-tab]] — секция «Agents»: код, API, питфоллы (шаг 4, не активирован) - [[personal/tech/macos-docker-virtiofs-mount-limits]] — ограничения macOS Docker - [[personal/tech/balda-go-fx-pitfalls]] — fx-питфоллы, дельта-апплаер - [[family/how-to/kraken-portainer-access]] — cloudflared, routes - скилл `eagle-dashboard` — архитектура дашборда ## История - **2026-09-26**: план создан; шаги 1-2 выполнены (образ, контейнер, мост на :8790). - **2026-09-26** (продолжение): мост переведён под launchd (шаг 3 ✅, `com.agents.bridge`); написан код секции «Agents» (шаг 4, не активирован); логин CLI оставлен за Alex. Туннель (шаг 3 сети) — за Alex, в панели Cloudflare Zero Trust.