Files
obsidian-vault/personal/tech/agents-bridge.md
T

166 lines
7.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 плана):
```bash
cd ~/Developer/agents-bridge
~/Developer/eagle-dash/.venv/bin/python -m uvicorn app:app --host 127.0.0.1 --port 8790
```
Проверка:
```bash
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` только так:
```bash
/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, ведущий в закрытый порт).
---
## Порядок ручной приёмки (сработавший)
```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]] — общий план делегирования
## История
- **2026-09-26**: создан, поднят на :8790, протестирован (auth 401, идемпотентность,
сквозной вызов CLI). Не в launchd. Логин CLI — за Alex.