7.7 KiB
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 |
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 ещё не сделан, см. открытый пункт 4 плана):
cd ~/Developer/agents-bridge
~/Developer/eagle-dash/.venv/bin/python -m uvicorn app:app --host 127.0.0.1 --port 8790
Проверка:
curl -s http://127.0.0.1:8790/healthz
⚠️ На 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. Мост не переживает перезагрузку
Пока нет launchd-плейта com.agents.bridge, процесс живёт только в текущей сессии.
Это блокер для шага 3 (туннель опубликует URL, ведущий в закрытый порт).
Порядок ручной приёмки (сработавший)
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/macos-docker-virtiofs-mount-limits — почему bind-mount вместо symlink
- personal/projects/balda/balda-mm-delegation-plan — общий план делегирования
История
- 2026-09-26: создан, поднят на :8790, протестирован (auth 401, идемпотентность, сквозной вызов CLI). Не в launchd. Логин CLI — за Alex.