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

204 lines
10 KiB
Markdown
Executable File
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** (с 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"=<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. Мост слушает только `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`).