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

21 KiB
Executable File
Raw Blame History

Фаза 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/<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. Открыто

  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/.... Живьём не проверено (дашборд не перезапущен)

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

История

  • 2026-09-26: план создан; шаги 1-2 выполнены (образ, контейнер, мост на :8790).
  • 2026-09-26 (продолжение): мост переведён под launchd (шаг 3 ✅, com.agents.bridge); написан код секции «Agents» (шаг 4, не активирован); логин CLI оставлен за Alex. Туннель (шаг 3 сети) — за Alex, в панели Cloudflare Zero Trust.