11 KiB
Context
Мост из bridge-core: хаб (jira-hub serve, Mac mini, launchd, 0.0.0.0:8790) и агент (agent.py на виртуалке,
Python 3.9, stdlib). Агент обращается к хабу шестью запросами (claim с ожиданием до 30 с, settings, commands/{id},
result, state, known-keys); все идут через Http.call() агента, ошибки сведены к ConnectionError (хаб
недоступен) и HttpError (код ответа). Повторы отчёта, журнал claimed/started/finished/acked и восстановление опираются
только на эти два исключения. В хабе serve() уже держит фоновый поток ticker.
При включённом VPN «Континент ZTN» сеть между виртуалкой и Mac mini закрыта (см. proposal.md). Общая папка UTM (VirtioFS) проверена: круг 0,75–0,82 с, атомарное переименование видно целиком, удаления видны с обеих сторон.
Goals / Non-Goals
Goals: агент работает с хабом при включённом Континенте; всё, что проверено прогоном на моке Jira 12.10.2026 (пакет всех типов, повтор без записей, остановка в паузе, обрыв Jira), работает так же через папку; HTTP API хаба не меняется. Non-goals — см. proposal.md.
Decisions
- Реле внутри
serve(вариант A, решение владельца). Один сервис под launchd, один лог. Поток реле стартует всегда и спит, покаrelay_dirпуст; включение и выключение — правкой настройки, без перезапуска. - Реле — HTTP-клиент своего же хаба. Запрос из файла пересылается на
127.0.0.1:<port>с заголовкомAuthorizationиз файла. Маршрутизация, проверка токенов, лимит тела и коды ошибок остаются в одном месте. - Папка моста
<relay_dir>с подпапкамиreq/(запросы агента),resp/(ответы),work/(взятые реле запросы). Обе стороны пишут файл под именем с точкой и переименовывают в<id>.json; имена с точкой никто не читает. - Формат. Запрос:
{id, method, path, body, authorization, deadline};deadline— момент UTC (ISO 8601), после которого ответ агенту не нужен. Ответ:{id, status, body}. - Взятие запроса — переименование
req/<id>.json→work/<id>.json. Повторно его никто не возьмёт; каждый взятый запрос обрабатывается в своём потоке, потому чтоclaimдержит ответ до 30 с. - Ошибки агента — те же, что у HTTP. Ответ 2xx → тело; иной код →
HttpError(status, body); нет ответа кdeadline→ агент удаляет свой запрос (если его ещё не взяли) и выбрасываетConnectionError. Runner, журнал и восстановление не меняются. - Защита
claimпо запасу времени. Реле пересылаетPOST /agent/claim, только если доdeadlineосталось ≥ 35 с (хаб держитclaim≤ 30 с); иначе запрос выбрасывается без ответа. Агент ставит дляclaimdeadline= сейчас + max(timeout, 45) с, как сейчас ставит таймаут HTTP. Это закрывает потерю ответаclaimиз-за медленной папки; общий случай M-2 остаётся отложенным. - Просроченные запросы выбрасываются без ответа (агент уже ушёл с
ConnectionError). Еслиdeadlineистёк после пересылки, ответ всё равно пишется — его уберёт уборка. - Хаб недоступен изнутри (сбой соединения с
127.0.0.1) — ответ со статусом 502 и текстом ошибки. - Перезапуск реле: файлы в
work/удаляются с записью в лог; агент дождётся своегоdeadlineи повторит. - Уборка раз в минуту: ответы в
resp/старше 10 мин, запросы вreq/сdeadlineстарше 60 с, файлы с точкой старше 10 мин. - Папки нет (SSD не подключён) — одно предупреждение в лог, реле ждёт и проверяет снова; подпапки
req/,resp/,work/реле создаёт само, когдаrelay_dirсуществует. - Лог — одна строка на запрос в лог хаба:
relay <METHOD> <path> <status> <секунды>. - Агент выбирает транспорт по
hub_url:http://…/https://…— как сейчас,folder:<путь>— файловый клиент с тем же интерфейсомHubClient.checkпечатает «хаб: доступен через папку <путь>, настройки vN» или «НЕДОСТУПЕН» с причиной (нет папки; реле не ответило за 10 с). Версия агента — 2. - Токен агента остаётся в файле запроса. Общая папка доступна только Mac mini и виртуалке; пароль Jira туда не попадает (как и в HTTP-транспорте, хаб получает только результаты и состояние).
Risks / Trade-offs
- Доступ launchd к внешнему диску. macOS может не пустить процесс из launchd на
/Volumes/externalSSDбез разрешения «Съёмные тома» или «Полный доступ к диску». Проверяется первым шагом приёмки на настоящей папке; при отказе — выдать доступuv/python в «Конфиденциальность и безопасность». - Кеш VirtioFS даёт постоянные ~0,75 с на круг. Для команд с паузами 30–120 с несущественно;
refreshна 140 задач — около 10 обменов. - Токен агента в файлах общей папки живёт до ответа и уборки. Приемлемо для домашней пары машин.
- Опоздавший ответ на
result/state: агент повторит запрос;resultидемпотентен, повторная загрузкаstateперезаписывает те же строки зеркала.
Migration Plan
- Слить PR хаба, перезапустить launchd-задание хаба;
settings_set relay_dir "/Volumes/externalSSD/utm/rshb/jira-bridge". - Прогон на моке Jira через временную папку (сценарий 12.10.2026), затем короткий прогон через настоящую папку.
- Слить PR агента; на виртуалке заменить
agent.py, вagent.json—"hub_url": "folder:/Volumes/My Shared Files/RSHB/jira-bridge". - При включённом Континенте:
check(обе строки «доступен»), запуск агента,refresh full,jira-hub import, сверка с CSV. Откат:relay_dir = nullи прежнийhub_url(сеть работает только без Континента).
Decisions made without discussion
| Дата | Решение | Обоснование | Цена ошибки |
|---|---|---|---|
| 2026-10-12 | Имя файла — uuid4().hex агента; ответ под тем же именем |
уникальность без координации сторон | нет |
| 2026-10-12 | Тело ответа в файле — строка (как пришло от HTTP), агент разбирает JSON сам | реле не трактует ответы хаба; не-JSON ответ даёт ту же ошибку, что по HTTP | нет |
| 2026-10-12 | deadline — абсолютное время UTC, а не таймаут |
реле на другой машине сравнивает со своими часами; Mac mini и виртуалка синхронизируют время с одного роутера/NTP | расхождение часов > 10 с ломает защиту claim; лечится запасом или переходом на относительный срок |
| 2026-10-12 | Реле перечитывает relay_dir раз в 5 с, опрос req/ — каждые 0,1 с, агент опрашивает resp/ каждые 0,1 с |
быстрее не нужно (кеш VirtioFS ~0,75 с), реже — заметная задержка | лишние вызовы listdir — дёшево |
| 2026-10-12 | relay_dir — абсолютный путь строкой или null; относительный путь — 422 |
путь читает процесс launchd с неочевидным cwd | нет |
| 2026-10-12 | Нечитаемый JSON запроса или ответа перечитывается до 2 с, затем запрос выбрасывается (реле) / ConnectionError (агент) |
атомарное переименование делает это маловероятным (проверка: 0 случаев) | нет |
| 2026-10-12 | relay_dir не входит в настройки для агента (AGENT_KEYS) |
агенту путь на стороне Mac mini не нужен | нет |
| 2026-10-13 | В ARCHITECTURE.md граница «HTTP живёт только в api и client» дополнена relay (R-1) |
реле само ходит в HTTP своего хаба; иначе документ противоречит коду | нет |
| 2026-10-13 | В tests/test_py39.py агента в белый список stdlib добавлен uuid (R-2, подробно — jira-agent/openspec/changes/folder-transport/design.md) |
спека требует uuid4().hex |
нет |