82 lines
10 KiB
Markdown
82 lines
10 KiB
Markdown
## 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 не нужен | нет |
|