Files

18 KiB
Executable File
Raw Permalink Blame History

[!WARNING] Историческая схема Balda Мост Mac ↔ Kraken снят с эксплуатации 2026-09-29. Команды, контейнеры и статусы ниже относятся к тому периоду. Текущий запуск: personal/projects/balda/setup. Исполнение и границы приёмки: personal/projects/balda/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
# установка/переустановка
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 только так:

/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 открыт (это осознанно — по нему проверяют и туннель, и мониторинг), а чтение задач тоже закрыто:

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» по пустому ответу одной задачи.


Порядок ручной приёмки (сработавший)

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 одной задачей

# ожидаемо при незалогиненном 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/креды.


Связанное

История

  • 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.