Andrey Epifancev 36bfd95a87 docs(openspec): решения исполнения folder-transport R-1, R-2 (#5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 00:50:10 +04:00

11 KiB
Raw Blame History

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:<port> с заголовком Authorization из файла. Маршрутизация, проверка токенов, лимит тела и коды ошибок остаются в одном месте.
  3. Папка моста <relay_dir> с подпапками req/ (запросы агента), resp/ (ответы), work/ (взятые реле запросы). Обе стороны пишут файл под именем с точкой и переименовывают в <id>.json; имена с точкой никто не читает.
  4. Формат. Запрос: {id, method, path, body, authorization, deadline}; deadline — момент UTC (ISO 8601), после которого ответ агенту не нужен. Ответ: {id, status, body}.
  5. Взятие запроса — переименование req/<id>.json → work/<id>.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 <METHOD> <path> <status> <секунды>.
  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 не нужен нет
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 нет