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

232 lines
12 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` | `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 |
```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. Мост слушает `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-форвард).
---
## Порядок ручной приёмки (сработавший)
```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/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 работает.