12 KiB
Executable File
agents-bridge — мост Mac ↔ Kraken (FastAPI :8790)
Создан 2026-09-26. Компонент Фазы 4 personal/plans/balda-mm-phase4-agents (шаг 2 ✅). Назначение: принять задачу от оркестратора Балды (Kraken) по HTTP, запустить CLI-исполнитель в контейнере
balda-executor, вернуть вывод.
Расположение
| Что | Путь |
|---|---|
| Проект | ~/Developer/agents-bridge/ |
| Python | ~/Developer/eagle-dash/.venv/bin/python (3.14, fastapi 0.115.0, uvicorn 0.30.6) |
| Конфиг + секрет | ~/Developer/agents-bridge/.env (0600, не коммитить) |
| Шаблон | ~/Developer/agents-bridge/.env.example |
| Состояние | ~/Developer/agents-bridge/state.sqlite3 (WAL) |
| Лог (ручной запуск) | /tmp/agents-bridge.log |
| Контейнер-исполнитель | balda-executor (~/Docker/balda-executor/) |
Своего venv у проекта нет — используется venv дашборда (там уже fastapi/uvicorn/httpx).
Файлы
| Файл | Роль |
|---|---|
app.py |
роуты, проверка секрета, лимит параллельности, ThreadPoolExecutor |
store.py |
SQLite: таблица tasks, UNIQUE-индекс по idempotency_key, reap_stale() |
executor.py |
docker exec в контейнер; провайдеры claude/codex; таймаут; обрезка вывода |
config.py |
чтение .env встроенным парсером + Settings (frozen dataclass) |
requirements.txt |
fastapi==0.115.0, uvicorn==0.30.6, httpx, python-dotenv |
API
GET /healthz → статус моста и контейнера (без секрета)
POST /task → 202 | 200 (идемпотентный повтор) | 401 | 429 | 400
GET /task/{task_id} → статус + stdout/stderr/exit_code
GET /tasks?limit=N → последние задачи (для дашборда/отладки)
Авторизация: заголовок X-Bridge-Secret на всех /task*. Если BRIDGE_SECRET не
настроен — POST /task возвращает 503, мост не принимает задачи.
Идемпотентность: заголовок Idempotency-Key. Повтор возвращает тот же task_id
с HTTP 200 (первый ответ — 202). Гарантия на уровне БД: UNIQUE-индекс + IntegrityError
как обработка гонки. CLI запускается ровно один раз на задачу.
Лимит параллельности: MAX_CONCURRENT_TASKS (по умолчанию 1). При исчерпании —
429. Больше 2 задач под одним подписочным аккаунтом — риск бана за «unusual traffic».
Провайдеры
executor.PROVIDERS — argv для headless-запуска:
| Провайдер | argv |
|---|---|
claude |
claude -p --output-format text <prompt> |
codex |
codex exec --skip-git-repo-check <prompt> |
Дефолт — EXECUTOR_PROVIDER=claude. Команда собирается списком аргументов (без
shell=True), WORKDIR=/agents внутри контейнера — наружу не выйти.
Переменные окружения
| Ключ | Дефолт | Смысл |
|---|---|---|
BRIDGE_HOST |
0.0.0.0 |
слушать всю LAN (Kraken ходит по LAN). Было 127.0.0.1 |
BRIDGE_PORT |
8790 |
порт |
BRIDGE_SECRET |
— | обязателен, 64 симв. secrets.token_urlsafe(48) |
EXECUTOR_CONTAINER |
balda-executor |
имя контейнера |
EXECUTOR_PROVIDER |
claude |
claude или codex |
MAX_CONCURRENT_TASKS |
1 |
одновременно выполняемых |
TASK_TIMEOUT_SECONDS |
900 |
таймаут задачи, процесс убивается |
MAX_OUTPUT_BYTES |
200000 |
вывод обрезается |
DB_PATH |
state.sqlite3 |
SQLite |
Запуск
Под launchd (с 2026-09-26) — плейт com.agents.bridge:
| Параметр | Значение |
|---|---|
| Label | com.agents.bridge |
| Plist | ~/Library/LaunchAgents/com.agents.bridge.plist |
| Program | ~/Developer/eagle-dash/.venv/bin/python -m uvicorn app:app --host 0.0.0.0 --port 8790 |
| WorkingDirectory | /Users/admin/Developer/agents-bridge |
RunAtLoad / KeepAlive |
true / true |
| Логи | ~/Library/Logs/agents-bridge.log (+ .err.log) |
PATH в EnvironmentVariables |
/opt/homebrew/bin:... — launchd не читает shell-PATH |
# установка/переустановка
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.agents.bridge.plist
# перезапуск
launchctl kickstart -k gui/$(id -u)/com.agents.bridge
# статус (pid, last exit)
launchctl list | grep agents.bridge
# проверка
curl -s http://127.0.0.1:8790/healthz
Проверено 2026-09-26: bootstrap → exit 0, в launchctl list строка 3063 0 com.agents.bridge,
/healthz = status: ok. ⚠️ Процедура убийства pid для проверки KeepAlive не выполнялась —
команда требует kill -9 и была отклонена пользователем, не повторять без явной команды.
Ручной запуск (если launchd отключён) — тем же uvicorn, но это аварийный путь.
⚠️ На macOS docker compose не работает — только docker-compose (см. план, шаг 1).
Питфоллы
1. Not logged in — это ответ CLI, а не ошибка моста
Сквозная проба до логина CLI вернула:
status=failed exit_code=1 stdout='Not logged in · Please run /login'
Это признак рабочего конвейера: docker поднял процесс, claude ответил. Если видишь
эту строку — auth у исполнителя, а не мост. Логин делает Alex сам; копирование
auth-файлов из ~/.claude/~/.codex отклонено, не предлагать.
2. Редакция секретов Hermes искажает команды создания .env
python -c с f-строкой BRIDGE_SECRET={s} печатает в лог обрезанный фрагмент
(E...cret written) — редакция Hermes, файл на диске цел. Не «чинить».
Проверять .env только так:
/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. Зависшие задачи после рестарта
store.reap_stale() на старте помечает running-задачи старше TASK_TIMEOUT_SECONDS
как timeout с причиной bridge restarted while task was running. Иначе они навсегда
висят активными и блокируют слот лимита.
4. Мост не переживает перезагрузку — ✅ ЗАКРЫТО
Решено 2026-09-26: плейт com.agents.bridge (RunAtLoad + KeepAlive), поднят через
launchctl bootstrap. Шаг 3 (туннель) больше не блокирован.
⚠️ Остаётся непроверенным поведение KeepAlive при падении процесса: тест требовал
kill -9 по pid моста и был отклонён. Проверить можно launchctl kickstart -k
(штатный путь) либо перезагрузкой Mac.
5. launchd не видит docker — PATH задавать в плейте
ProgramArguments использует абсолютный путь к python, но executor.py вызывает
docker и docker-compose из PATH. При запуске под launchd PATH = /usr/bin:/bin,
поэтому docker (/opt/homebrew/bin/docker) не находится. В плейт добавлен блок
EnvironmentVariables.PATH с /opt/homebrew/bin первым. Симптом без него:
docker не найден в PATH процесса моста в stderr задачи.
6. Мост слушает 0.0.0.0 — изменение 2026-09-26
Было: BRIDGE_HOST=127.0.0.1 («наружу — через туннель»).
Стало: 0.0.0.0 — иначе Kraken не достучится по LAN.
Причина: Kraken и Mac в одной сети, ping 2.4 мс. Туннель не нужен (см.
personal/plans/balda-mm-phase4-agents §7). Kraken обращается к
http://192.168.1.57:8790.
⚠️ Порт 8790 теперь открыт в LAN. Защита — X-Bridge-Secret (без него POST /task → 503).
Уходя в недоверенную сеть — вернуть 127.0.0.1 и поднимать туннель.
🔴 --host задаётся в plist, .env его НЕ перебивает. Симптом: поменял .env,
перезапустил — lsof всё равно показывает 127.0.0.1:8790. Правка — в
ProgramArguments plist. И применять через bootout + bootstrap (не kickstart).
7. Обратный SSH-туннель на Kraken невозможен
ssh -R падает: Error: remote port forwarding failed for listen port 18790,
в verbose — debug1: Remote: Server has disabled port forwarding.
Причина: на Kraken в sshd AllowTcpForwarding no. Не путать с «порт занят» —
порт свободен, форвард выключен администратором. Обход — Cloudflare Tunnel
(исходящее соединение, не SSH-форвард).
Порядок ручной приёмки (сработавший)
curl -s -o /dev/null -w "%{http_code}\n" -X POST http://127.0.0.1:8790/task \
-H 'Content-Type: application/json' -d '{"prompt":"hi"}' # → 401 (нет секрета)
# с секретом и одним Idempotency-Key дважды → один и тот же task_id, 202 затем 200
# GET /task/{id} → status/stdout/exit_code
Тестовый скрипт: /tmp/test-bridge.sh (читает секрет из .env, в командной строке
секрет не светится).
Связанное
- personal/plans/balda-mm-phase4-agents — план Фазы 4 (шаги 1-2 ✅)
- personal/tech/balda-executor-mcp — MCP-сервер, который ходит в этот мост
- personal/tech/macos-docker-virtiofs-mount-limits — почему bind-mount вместо symlink
- personal/projects/balda/balda-mm-delegation-plan — общий план делегирования
~/Developer/agents-bridge— код моста
История
- 2026-09-26: создан, поднят на :8790, протестирован (auth 401, идемпотентность, сквозной вызов CLI). Логин CLI — за Alex.
- 2026-09-26 (продолжение): переведён под launchd (
com.agents.bridge,RunAtLoad+KeepAlive, pid 3063), добавленEnvironmentVariables.PATH. Шаг 3 разблокирован; проверкаKeepAliveпри падении не выполнена (отклонёнkill -9). - 2026-09-26 (шаг 5):
BRIDGE_HOST→0.0.0.0(в plist, не в.env), сервис переставлен черезbootout+bootstrap. Проверено с Kraken:curlс хоста и из контейнера вmattermost_mm-netдаютstatus: ok. Туннель не потребовался — Kraken и Mac в одной LAN. Обратный SSH-туннель на Kraken невозможен (AllowTcpForwarding no). - 2026-09-26 (MCP):
executor_healthиз контейнераbalda-executor-mcp(docker-сеть Kraken) вернул живые данные моста — связка Kraken→Mac работает.