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

295 lines
18 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.
> [!WARNING] Историческая схема Balda
> Мост Mac ↔ Kraken снят с эксплуатации 2026-09-29. Команды, контейнеры и статусы ниже относятся к тому периоду.
> Текущий запуск: [[personal/projects/balda/setup|Balda — актуальная конфигурация]]. Исполнение и границы приёмки: [[personal/projects/balda/execution-broker|Execution Broker]].
# agents-bridge — мост Mac ↔ Kraken (FastAPI :8790)
> Создан 2026-09-26. Компонент Фазы 4 [[personal/plans/balda-mm-phase4-agents]] (шаг 2 ✅).
> Назначение: принять задачу от оркестратора Балды (Kraken) по HTTP, запустить
> CLI-исполнитель в контейнере `balda-executor`, вернуть вывод.
> 🔴 2026-09-29: провайдер меняется с `claude` (подписка) на **`callee`** (DeepSeek) —
> см. §Провайдеры и [[personal/plans/balda-callee-prism-plan]].
---
## Расположение
| Что | Путь |
|---|---|
| Проект | `~/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>` | ⛔ по той же причине |
| `callee` | `callee agent run prism/lifecycle --message <prompt>` | 🔜 **целевой** (план [[personal/plans/balda-callee-prism-plan]], фаза C3) |
Дефолт — `EXECUTOR_PROVIDER=claude` → **меняется на `callee`**. Команда собирается
**списком аргументов** (без `shell=True`), `WORKDIR=/agents` внутри контейнера — наружу не выйти.
⚠️ **При смене argv промпт обязан остаться ОДНИМ последним элементом списка** —
`["docker","exec","-w","/agents","@container", *base, prompt]`. Вставка аргументов после
`prompt` рвёт передачу.
---
## Переменные окружения
| Ключ | Дефолт | Смысл |
|---|---|---|
| `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-форвард).
### 8. 🔴 `GET /task/{id}` и `/tasks` требуют секрет; `/healthz` — нет
Проверено 2026-09-28. Асимметрия: `/healthz` открыт (это осознанно — по нему проверяют
и туннель, и мониторинг), а **чтение задач тоже закрыто**:
```bash
curl -s http://127.0.0.1:8790/tasks # → {"detail":"unauthorized"}
curl -s -H "X-Bridge-Secret: $SECRET" .../tasks # → JSON со списком
```
Симптом ошибки: `jq` падает `Cannot index object with object` — потому что в ответе
объект `{"detail":"unauthorized"}`, а не массив. **Не `jq` сломан** — просто нет
заголовка. Всегда проверять `head -c 200` перед разбором.
📌 Полезно: ответ `GET /task/{id}` достаточно детален, чтобы поставить диагноз без логов
контейнера — `status`, `exit_code`, `duration_s`, `stdout`, `stderr`, `error`.
`duration_s < 1` при `exit_code: 1` = CLI упал мгновенно на своей проверке (напр.
авторизации), а не на реальной работе.
### 9. 🔴 Кратковременные задачи могут стать `failed` без `stdout`/`error`
В списке `/tasks` видны исторические задачи с `exit_code: 1`, `duration_s ~0.7-0.8`,
`stdout: null`, `error: null` — тогда как другая задача того же класса отдала
содержательный `error` (`Claude configuration file not found at: /root/.claude.json`).
Следствие: **пустой `stdout` не значит «задача не запускалась».** Смотреть на связку
`status=failed` + `exit_code=1` + `duration_s`, а причину добирать из логов контейнера
(`docker logs balda-executor`) или повтором задачи. Не делать вывод «мост не вызывает CLI»
по пустому ответу одной задачи.
---
## Порядок ручной приёмки (сработавший)
```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 (⚠️ С секретом, иначе unauthorized)
```
📌 **Секрет из `.env` в inline-команде собирать скриптом, а не литералом.** Строка вида
`SECRET=$(grep … .env | cut …)` в инлайн-шелле ломается на кавычках и на редакции секретов
Hermes — рабочий вариант: положить в файл (`write_file`) и запустить `bash /tmp/x.sh`.
Секрет в командной строке не светится.
Тестовый скрипт: `/tmp/test-bridge.sh`, `/tmp/probe-exec.sh` (читают секрет из `.env`).
### Проверка авторизации CLI одной задачей
```bash
# ожидаемо при незалогиненном CLI: status=failed, exit_code=1, duration_s<1,
# stdout="Not logged in · Please run /login"
# при рабочем логине: status=succeeded, реальный текст модели
```
`/tmp/probe-exec.sh <task_id>` — печатает полный JSON задачи **и** состояние ФС
контейнера (`ls -la /root/`), чтобы сразу видеть, есть ли `.claude.json`/креды.
---
## Связанное
- [[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 работает.
- **2026-09-28**: аудит Фазы 4 живыми вызовами. Мост + туннель + контейнер исправны,
задача доходит до CLI, но падает `Not logged in · Please run /login` — **CLI в контейнере
не авторизован**, это единственный блокер Фазы 4. В хост-монтировании `~/cli-auth/claude/`
**нет** `.claude.json` — только `backups/`, `projects/`, `sessions/`. Добавлены §8
(чтение задач требует секрет) и §9 (failed-задачи без stdout). Разбор:
[[personal/plans/balda-mm-phase4-agents]] §20.