# 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 ` | | `codex` | `codex exec --skip-git-repo-check ` | Дефолт — `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** (с 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 127.0.0.1 --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 | ```bash # установка/переустановка 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` только так: ```bash /usr/bin/awk -F= '{ if ($1 ~ /SECRET|TOKEN|KEY|PASSWORD/) print $1"="; 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. Мост слушает только `127.0.0.1` `BRIDGE_HOST=127.0.0.1` — намеренно. Наружу выставляется туннелем (шаг 3), не пробросом порта. Не менять на `0.0.0.0` без Access Application на стороне Cloudflare. --- ## Порядок ручной приёмки (сработавший) ```bash 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]] — общий план делегирования - `~/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`).