## 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 1. **Реле внутри `serve` (вариант A, решение владельца).** Один сервис под launchd, один лог. Поток реле стартует всегда и спит, пока `relay_dir` пуст; включение и выключение — правкой настройки, без перезапуска. 2. **Реле — HTTP-клиент своего же хаба.** Запрос из файла пересылается на `127.0.0.1:` с заголовком `Authorization` из файла. Маршрутизация, проверка токенов, лимит тела и коды ошибок остаются в одном месте. 3. **Папка моста** `` с подпапками `req/` (запросы агента), `resp/` (ответы), `work/` (взятые реле запросы). Обе стороны пишут файл под именем с точкой и переименовывают в `.json`; имена с точкой никто не читает. 4. **Формат.** Запрос: `{id, method, path, body, authorization, deadline}`; `deadline` — момент UTC (ISO 8601), после которого ответ агенту не нужен. Ответ: `{id, status, body}`. 5. **Взятие запроса — переименование `req/.json` → `work/.json`.** Повторно его никто не возьмёт; каждый взятый запрос обрабатывается в своём потоке, потому что `claim` держит ответ до 30 с. 6. **Ошибки агента — те же, что у HTTP.** Ответ 2xx → тело; иной код → `HttpError(status, body)`; нет ответа к `deadline` → агент удаляет свой запрос (если его ещё не взяли) и выбрасывает `ConnectionError`. Runner, журнал и восстановление не меняются. 7. **Защита `claim` по запасу времени.** Реле пересылает `POST /agent/claim`, только если до `deadline` осталось ≥ 35 с (хаб держит `claim` ≤ 30 с); иначе запрос выбрасывается без ответа. Агент ставит для `claim` `deadline` = сейчас + max(timeout, 45) с, как сейчас ставит таймаут HTTP. Это закрывает потерю ответа `claim` из-за медленной папки; общий случай M-2 остаётся отложенным. 8. **Просроченные запросы выбрасываются без ответа** (агент уже ушёл с `ConnectionError`). Если `deadline` истёк после пересылки, ответ всё равно пишется — его уберёт уборка. 9. **Хаб недоступен изнутри** (сбой соединения с `127.0.0.1`) — ответ со статусом 502 и текстом ошибки. 10. **Перезапуск реле**: файлы в `work/` удаляются с записью в лог; агент дождётся своего `deadline` и повторит. 11. **Уборка раз в минуту**: ответы в `resp/` старше 10 мин, запросы в `req/` с `deadline` старше 60 с, файлы с точкой старше 10 мин. 12. **Папки нет** (SSD не подключён) — одно предупреждение в лог, реле ждёт и проверяет снова; подпапки `req/`, `resp/`, `work/` реле создаёт само, когда `relay_dir` существует. 13. **Лог** — одна строка на запрос в лог хаба: `relay <секунды>`. 14. **Агент выбирает транспорт по `hub_url`**: `http://…`/`https://…` — как сейчас, `folder:<путь>` — файловый клиент с тем же интерфейсом `HubClient`. `check` печатает «хаб: доступен через папку <путь>, настройки vN» или «НЕДОСТУПЕН» с причиной (нет папки; реле не ответило за 10 с). Версия агента — 2. 15. **Токен агента остаётся в файле запроса.** Общая папка доступна только 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 1. Слить PR хаба, перезапустить launchd-задание хаба; `settings_set relay_dir "/Volumes/externalSSD/utm/rshb/jira-bridge"`. 2. Прогон на моке Jira через временную папку (сценарий 12.10.2026), затем короткий прогон через настоящую папку. 3. Слить PR агента; на виртуалке заменить `agent.py`, в `agent.json` — `"hub_url": "folder:/Volumes/My Shared Files/RSHB/jira-bridge"`. 4. При включённом Континенте: `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 не нужен | нет |