21 KiB
Executable File
Фаза 4 — исполнитель на Mac + ~/agents + секция «Agents» в Eagle Dashboard
План. Шаги 1-3 выполнены (2026-09-26): образ собран, контейнер
balda-executorUp (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/<proj>:/agents/<name>: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/<name> |
| Подключить внешний | запись в 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).
Сборка образа (сработавшая команда)
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:
{"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):
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 (туннель вёл бы в закрытый порт) снят: мост поднимается при входе и перезапускается при падении.
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 с маскировкой:
/usr/bin/awk -F= '{ if ($1 ~ /SECRET|TOKEN|KEY|PASSWORD/) print $1"=<redacted len "length($2)">"; 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ктивация:
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. Открыто
Провайдер исполнителя→ claude (оба в образе, переключениеEXECUTOR_PROVIDER)Нужен ли→ приask-апрувallow_allне нужен; при потребности начинать с B (апрув в мосте, ~0.5 дня)- 🔴 Логин CLI — делает Alex сам. Копирование auth-файлов отклонено; до логина задачи
возвращают
Not logged in Мост не в launchd→ ✅ закрыто:com.agents.bridge(RunAtLoad+KeepAlive)- 🔴 Активация секции «Agents» — код готов, нужен
__pycache__+kickstart -k(апрув не получен) - ⚠️ Конфликт за порт 8790 —
agents_bridgeвservices.yamlобъявленtype: daemon, но мост уже управляется своим launchd. Ревизия при активации:type: system+health.type: launchctlлибоtype: none. См. personal/tech/eagle-dashboard-agents-tab §4 - ⚠️ Вкладка
?tab=agentsвtemplates/index.htmlне сделана — API готов, UI нет - Hostname моста —
agents-bridge.qentra.top(шаг 3, public route добавляет Alex в CF) - Порт
8790— свободен до старта моста, занят после; иных слушателей нет - ⚠️ Под 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.