28 KiB
hub-api Specification
Purpose
HTTP API хаба jira-hub serve: маршруты агента (выдача команд, отчёты, загрузка состояния), маршруты интерфейсов (зеркало, заметки, пакеты, настройки), резервные копии, автообновление зеркала и реле общей папки для агента за VPN.
Requirements
Requirement: Процесс и доступ
jira-hub serve SHALL запускать один процесс: HTTP на 0.0.0.0:<port> (по умолчанию 8790), база и настройки в
~/.local/share/jira-hub/ (переопределяется JIRA_HUB_HOME). Все маршруты API SHALL требовать
Authorization: Bearer <token>: агентский токен открывает только /agent/*, клиентский — всё остальное. Исключение —
страница доски связей (GET /, GET /links): это статический файл без данных, он отдаётся без токена. Ответы API —
JSON, ошибки — {error, detail} с кодами 400 (неверное тело или Content-Length), 401, 404, 409, 413 (тело больше
предела), 422, 500 (внутренняя ошибка); реле общей папки дополнительно отвечает 502. Методы, кроме GET, POST и PUT,
отвечают 501 стандартной страницей http.server, не JSON.
Scenario: Чужой токен
- WHEN агентским токеном вызван
GET /issues - THEN 401
Scenario: Неподдерживаемый метод
- WHEN
DELETE /issues/AURORA-1 - THEN 501, тело не JSON; база не меняется
Scenario: Страница без токена
- WHEN
GET /без заголовкаAuthorization - THEN 200,
Content-Type: text/html; charset=utf-8, тело — страница доски связей; данных зеркала в ней нет
Requirement: Маршруты агента
Хаб SHALL предоставлять агенту следующие маршруты (все под агентским токеном):
POST /agent/claim{agent_version, settings_version, last_jira_call_at, update_state, launcher_version}— долгий запрос до 30 с; ответ{command: {id, type, params, pause_seconds}, settings_version, agent_release}или{command: null, settings_version, agent_release}, гдеagent_release—{version, sha256}опубликованной версии агента илиnull; обновляетagent_status(last_jira_call_at = nullне затирает прежнее значение).update_state(объект илиnull,errorхранится до 500 символов) иlauncher_version(целое илиnull) необязательны; хаб SHALL хранить последние значения в памяти процесса, значения неверного типа игнорировать, не отвергаяclaim. Если есть команда вclaimed,claimSHALL выдать её снова (повторная выдача, см.commands), а не следующую из очереди.GET /agent/settings— настройки для агента (без токенов).GET /agent/commands/{id}— команда по id (для восстановления из журнала), включаяpause_seconds.POST /agent/commands/{id}/result{status, result, error, jira_calls, rate_limited}— идемпотентно: повторный отчёт по команде в терминальном статусе отвечает 200 без изменений, кроме позднего отчётаdoneсresult.keyпоensure_issueвcancelledилиfailedбез результата (результат записывается, статус не меняется, заметкаnew:Nпереносится на ключ). Отчёт по команде вqueuedSHALL отвергаться 409not_claimed.errorхаб SHALL обрезать до 1500 символов.POST /agent/state{command_id, issues: [...], links: [...], neighbor_keys: [...], page, last_page}— загрузка состояния пачками;neighbor_keys— ключи вне базовой JQL, дочитанные по связям; хаб обновляет зеркало иsync_run. Каждая пачка применяется независимо;pageиlast_pageнеобязательны и хабом не используются.GET /agent/known-keys— ключи зеркала (для определения соседей).GET /agent/release— опубликованная версия агента{version, sha256, content}(content— текстagent.py); нет публикации или файл не сходится с манифестом по sha256 — 404no_release.
Scenario: Выдача с подстановкой ключа
- WHEN следующая команда ссылается на
new:60, аensure_issueдляnew:60завершилась сAURORA-1590 - THEN
claimотдаёт команду сAURORA-1590в параметрах
Scenario: Повторный отчёт
- WHEN агент дважды прислал
resultдля команды 15 - THEN второй ответ 200, статус и результат не изменились
Scenario: Отчёт по невыданной команде
- WHEN
POST /agent/commands/16/resultсstatus = done, команда 16 вqueued - THEN 409
not_claimed, команда осталасьqueued
Scenario: Длинная ошибка
- WHEN агент прислал
failedсerrorдлиной 5000 символов - THEN в
command.errorпервые 1500 символов
Scenario: Поздний отчёт после предела выдач
- WHEN
ensure_issue {temp_key: new:8, …}переведена хабом вfailed«агент не отчитался после 3 выдач», затем агент прислалdoneсresult.key = AURORA-1630 - THEN команда
failed, вresult—AURORA-1630; заметкаnew:8перенесена наAURORA-1630
Scenario: Публикация в ответе claim
- WHEN опубликована v6, агент v5 отправил
claim, команд нет - THEN ответ
{command: null, settings_version, agent_release: {version: 6, sha256: …}}
Scenario: Состояние обновления от агента
- WHEN агент прислал в
claimupdate_state = {state: rolled_back, from_version: 6, to_version: 7, error: "код 1 через 3 с"}иlauncher_version = 1 - THEN
GET /agentотдаёт этоupdate_state,launcher_version = 1иupdate_hint«откат с v7 на v6: код 1 через 3 с»
Scenario: Скачивание версии
- WHEN агентским токеном
GET /agent/release, опубликована v6 - THEN 200
{version: 6, sha256, content}, sha256 отcontentв UTF-8 равенsha256; клиентским токеном — 401; без публикации — 404no_release
Requirement: Маршруты интерфейсов
Хаб SHALL предоставлять интерфейсам (MCP, позже просмотрщик) следующие маршруты под клиентским токеном:
GET /issuesс фильтрамиsprint,assignee,work_kind,fact_status,status,label,epic,neighbor,q(подстрока темы),limit/offset— строки зеркала (все поляissue) сfact_statusиwork_kindиз заметок; сокращение до карточки — дело интерфейса.GET /issues/{key}— задача целиком: поля, связи в обе стороны, заметки, последние 10 команд по ней.PUT /issues/{key}/note— заметки (частичное обновление,updated_byиз тела).POST /batches{title, author, commands: [...]}→{id, estimate_seconds}; 422 при неизвестном типе, неверных параметрах (в том числе неверного типа значения), временном ключе безensure_issue, двух и болееensure_issueс одинаковойsummary(сравнение после обрезки пробелов по краям; в ответе перечислены повторяющиеся темы).GET /batches?limit=,GET /batches/{id}(с командами),POST /batches/{id}/cancel— отменяетqueuedкоманды пакета; ответ — вид пакета с полемstill_claimed: список{id, type}команд пакета вclaimed(они будут выполнены агентом; остановить — отмена команды сforce).POST /commands/{id}/retry— новая команда с теми же параметрами в новом пакете «Повтор команды N».POST /commands/{id}/cancel— только дляqueued; дляclaimed— 409, если не переданforce: true.POST /refresh{keys | jql | full: true}→ пакет из одногоrefresh.GET /agent,GET /sync-runs?limit=,GET /settings,PUT /settings(частично; поднимаетsettings_version).
Scenario: Фильтр по спринту
- WHEN
GET /issues?sprint=Спринт 2 - THEN только задачи с таким
sprint, отсортированы по сроку, затем по ключу
Scenario: Отмена взятой команды
- WHEN
POST /commands/15/cancelбезforce, командаclaimed - THEN 409 с
claimed_at; сforce: true—cancelled, следующий отчёт агента по ней принимается, статус не меняется
Scenario: Поздний отчёт по отменённому созданию
- WHEN
ensure_issue {temp_key: new:7, …}отменена сforce: true, затем агент прислалdoneсresult.key = AURORA-1620 - THEN команда
cancelled, вresult—AURORA-1620; заметкаnew:7перенесена наAURORA-1620
Scenario: Дубль темы в пакете
- WHEN
POST /batchesс двумяensure_issue, у обеихsummary = "Шахматка: комментарии" - THEN 422 с темой «Шахматка: комментарии», пакет не создан
Scenario: Тема не строка
- WHEN
POST /batchesсensure_issue {temp_key: new:1, summary: 42, …} - THEN 422 с указанием параметра
summary, пакет не создан
Scenario: Отмена пакета с командой у агента
- WHEN в пакете 9 команда 30
claimed, команды 31 и 32queued;POST /batches/9/cancel - THEN 31 и 32
cancelled; в ответеstill_claimed = [{id: 30, type: …}]; 30 остаётсяclaimed
Requirement: Резервные копии
Перед выдачей первой команды пакета, содержащего записи в Jira, хаб SHALL скопировать файл базы в backups/<дата-время>-<batch>.sqlite
и хранить последние backup_keep копий.
Scenario: Ротация
- WHEN копий больше
backup_keep - THEN самые старые удаляются до
backup_keep
Requirement: Автообновление зеркала
Если auto_refresh_minutes > 0 и агент на связи, хаб SHALL поставить пакет «Автообновление» из refresh {jql: base_jql},
когда в очереди нет команд в queued и claimed и последний sync_run начат раньше, чем auto_refresh_minutes назад
(или sync_run ещё нет).
Scenario: Очередь занята
- WHEN интервал прошёл, но в очереди есть
queuedкоманды - THEN автообновление не ставится до опустошения очереди
Scenario: Есть выданная команда без отчёта
- WHEN интервал прошёл,
queuedнет, одна команда вclaimed - THEN автообновление не ставится; команда довыдаётся при следующем
claim
Scenario: Недавнее обновление
- WHEN очередь пуста,
auto_refresh_minutes = 60, последнийsync_runначат 20 минут назад - THEN автообновление не ставится
Requirement: Реле через общую папку
jira-hub serve SHALL запускать поток реле вместе с HTTP-сервером. Пока настройка relay_dir пуста (null), реле
бездействует; настройку оно SHALL перечитывать не реже раза в 5 с, включение и выключение не требуют перезапуска. Если
каталог relay_dir существует, реле SHALL создать в нём подкаталоги req/, resp/, work/; если не существует —
записать одно предупреждение в лог и проверять снова.
Обмен:
- запрос агента — файл
req/<id>.json:{id, method, path, body, authorization, deadline}, гдеdeadline— момент UTC (ISO 8601), после которого ответ не нужен; файлы, имя которых начинается с точки, реле MUST NOT читать; - реле SHALL брать запрос переименованием в
work/<id>.jsonи обрабатывать каждый взятый запрос в отдельном потоке; - запрос с истёкшим
deadlineSHALL выбрасываться без ответа;POST /agent/claimSHALL пересылаться, только если доdeadlineосталось не меньше 35 с, иначе выбрасываться без ответа; - остальные запросы реле SHALL пересылать в свой HTTP на
<host>:<port>процессаserve(127.0.0.1, еслиserveслушает0.0.0.0) тем же методом, путём и телом, с заголовкомAuthorizationиз файла, и записывать ответresp/<id>.json={id, status, body}(тело — строка ответа HTTP) через временный файл с именем на точку и переименование; затем удалятьwork/<id>.json; - если соединение со своим HTTP не удалось или ответ нарушает протокол HTTP, ответ SHALL иметь
status = 502и текст ошибки вbody; - нечитаемый JSON запроса SHALL перечитываться до 2 с, затем запрос выбрасывается с записью в лог.
При первой подготовке каждой папки после старта реле (в том числе новой папки после смены relay_dir) реле SHALL
удалить файлы из work/ с записью в лог. Повторная подготовка той же папки (пропал подкаталог, не было доступа) SHALL
только создавать недостающие подкаталоги, не трогая work/. Раз в минуту
реле SHALL удалять ответы в resp/ старше 10 мин, запросы в req/ с deadline старше 60 с и файлы с именем на точку
старше 10 мин. На каждый обработанный запрос реле SHALL писать в лог хаба строку relay <METHOD> <path> <status> <секунды>. HTTP API, обработчики и проверка токенов при этом не меняются.
Scenario: Запрос через папку
- WHEN в
req/появилсяa1.jsonсGET /agent/settings, агентским токеном иdeadlineчерез 60 с - THEN в
resp/появляетсяa1.jsonсоstatus = 200и телом настроек;work/a1.jsonудалён
Scenario: Чужой токен через папку
- WHEN в файле запроса
GET /issuesс агентским токеном - THEN ответ со
status = 401, как по HTTP
Scenario: Просроченный claim
- WHEN реле взяло
POST /agent/claim, доdeadlineкоторого осталось 20 с - THEN запрос выброшен без ответа и без обращения к хабу; команда в очереди осталась
queued
Scenario: Долгий claim не задерживает другие запросы
- WHEN реле пересылает
claim, который ждёт команду 30 с, и в это время вreq/появляетсяGET /agent/settings - THEN ответ на
settingsпишется, не дожидаясьclaim
Scenario: Папка не подключена
- WHEN
relay_dirзадан, но каталога нет; через минуту каталог появился - THEN в логе одно предупреждение; после появления каталога реле создаёт подкаталоги и обрабатывает запросы
Scenario: Сбой протокола своего HTTP
- WHEN при пересылке запроса свой HTTP оборвал ответ (
http.client.HTTPException) - THEN в
resp/ответ соstatus = 502и текстом ошибки; реле продолжает работу
Scenario: Повторная подготовка не трогает взятые запросы
- WHEN реле работает, в
work/лежит взятый запрос, и пропал подкаталогresp/ - THEN реле создаёт
resp/заново; файл вwork/не удалён, в логе нет «удалён незавершённый запрос»
Requirement: Снимок доски
Хаб SHALL предоставлять GET /board под клиентским токеном — согласованный снимок зеркала для доски связей, прочитанный
в одной транзакции:
generated_at— момент снимка (UTC);last_sync— последний завершённый полный прогон (refreshпо базовой JQL):{started_at, finished_at, issues_count}илиnull, если такого не было;issues— все задачи зеркала, включая эпики и соседей:key,summary,issue_type,status,priority,assignee,sprint,duedate,planned_start,planned_end,epic_key,labels(список),neighbor(bool),fact_status,work_kind(из заметок,nullесли заметки нет); порядок — по ключу;links— все связи зеркала:{source, target, link_type},link_type— имя типа в Jira, направление как вissue_link(sourceблокирует/ссылается наtarget). Перевод в модель доски (вид работы по меткам, имена эпиков, логические типы связей) — дело интерфейса; хаб отдаёт данные зеркала без преобразований.
Scenario: Снимок с задачами и связью
- WHEN в зеркале эпик AURORA-1351, задачи AURORA-1620 (метка
kind-dev, эпик AURORA-1351) и AURORA-1621, связьAURORA-1620 → AURORA-1621типаASUPR_Блокер_Начала, заметкаwork_kind = "разработка"у AURORA-1620 - THEN
GET /boardотдаёт три задачи по ключу, у AURORA-1620labels = ["kind-dev"],epic_key = "AURORA-1351",work_kind = "разработка"; вlinks—{source: "AURORA-1620", target: "AURORA-1621", link_type: "ASUPR_Блокер_Начала"}
Scenario: Последний полный прогон
- WHEN в журнале синхронизаций полный прогон, завершённый в 10:00, и после него обновление по ключам в 11:00
- THEN
last_sync.finished_at— 10:00
Scenario: Без токена
- WHEN
GET /boardбез токена или с агентским токеном - THEN 401
Requirement: Страница доски связей
Хаб SHALL отдавать по GET / и GET /links страницу доски связей — один HTML-файл из пакета хаба
(src/jira_hub/web/links.html, развитие просмотрщика «Спринт 2 линк»). Страница SHALL:
- спрашивать клиентский токен хаба при первом открытии и после ответа 401, хранить его в
localStorageбраузера и передавать в заголовкеAuthorizationсвоих запросов к API того же хоста; - при открытии загружать
GET /boardи строить доску из него: задачи-эпики — дорожки, остальные задачи (включая соседей) — карточки; вид работы — по меткамkind-*, иначе по типу задачи; срок карточки —planned_end, при его отсутствииduedate; связи — по известным странице типам Jira, неизвестные типы пропускаются; - показывать в шапке время последнего полного прогона (
last_sync.finished_at); - по кнопке «Обновить из Jira» ставить
POST /refresh {full: true, author: "viewer"}, показывать ход пакета поGET /batches/{id}до завершения и затем перечитыватьGET /board; если агент офлайн (GET /agent), предупреждать, но пакет ставить; при ошибке пакета — показывать её текст; - по кнопке «Перечитать» загружать
GET /boardзаново без обращения к Jira; - сохранять раскладку карточек, порядок и свёртку дорожек, фильтры в
localStorageпод ключами, не зависящими от источника данных; импорт JSON, выгруженного страницей, в режиме хаба SHALL сохранять эти настройки и сразу перечитывать данные хаба, а не показывать снимок из файла; - оставлять загрузку CSV из фильтра Jira как запасной путь (снимок с пометкой «из файла»; «Перечитать» возвращает данные хаба);
- при недоступном хабе показывать понятное сообщение и предлагать загрузить файл; встроенной выгрузки данных страница не содержит.
Scenario: Первый вход
- WHEN страница открыта впервые, токена в
localStorageнет - THEN вместо доски — поле клиентского токена с подсказкой
uv run jira-hub tokens; после ввода верного токена доска строится изGET /board, токен сохранён
Scenario: Неверный токен
- WHEN сохранённый токен устарел и
GET /boardответил 401 - THEN страница снова показывает поле токена с пояснением, что токен не подошёл
Scenario: Обновление из Jira
- WHEN нажата «Обновить из Jira», агент на связи
- THEN в хабе пакет из одного
refreshпо базовой JQL с авторомviewer; страница показывает «пакет #N выполняется», по завершении перечитывает доску и показывает новое время данных
Scenario: Перенос раскладки
- WHEN в режиме хаба импортирован JSON, выгруженный старой страницей «Спринт 2 линк»
- THEN раскладка, дорожки и фильтры из JSON применены к задачам хаба; задачи и связи — из хаба, не из JSON
Requirement: Публикация версии агента
jira-hub agent publish PATH SHALL публиковать файл агента: текст в UTF-8 не больше 1 МБ, ровно одна строка
AGENT_VERSION = <целое>, версия больше опубликованной. Файл SHALL записываться в <JIRA_HUB_HOME>/release/agent.py
(каталог 0700), затем manifest.json {version, sha256, size, published_at}; оба — через временный файл и
переименование, манифест последним. Команда SHALL работать без запущенного serve и печатать «опубликована v,
sha256 <первые 12 символов>…, байт»; при ошибке — сообщение и код 1, опубликованное не меняется. Хаб SHALL
читать манифест при каждом claim, GET /agent/release и GET /agent; битый манифест означает «публикации нет»
(предупреждение в лог). GET /agent SHALL дополняться полями release ({version, sha256, published_at} или null),
update_state, launcher_version и update_hint: «агент запущен без run.py — обновится только вручную» (опубликована
версия выше версии агента, агент онлайн, launcher_version = null), «откат с v на v: »
(rolled_back), «v отвергнута: » (rejected), иначе null.
Scenario: Публикация новой версии
- WHEN опубликована v5,
jira-hub agent publish jira-agent/src/agent.pyсAGENT_VERSION = 6 - THEN в
release/— новыйagent.pyи манифест сversion = 6; вывод «опубликована v6, sha256 …»; код 0
Scenario: Версия не выше опубликованной
- WHEN опубликована v6, публикуется файл с
AGENT_VERSION = 6 - THEN «опубликована v6, новая должна быть больше», код 1;
release/не изменён
Scenario: Нет строки версии
- WHEN публикуется файл без строки
AGENT_VERSION = <целое>или с двумя такими строками - THEN сообщение об ошибке, код 1;
release/не изменён
Scenario: Агент без запускателя
- WHEN опубликована v6, агент v5 онлайн, в последнем
claimlauncher_version = null - THEN
GET /agent→update_hint= «агент запущен без run.py — обновится только вручную»