Andrey Epifancev ca231ba2ae docs(openspec): архив agent-self-update (#22)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 22:58:46 +04:00

142 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## MODIFIED Requirements
### Requirement: Состав и настройки агента
Агент SHALL состоять из файла `agent.py` (Python ≥ 3.9, только стандартная библиотека), запускателя `run.py` (те же
ограничения) и файла `agent.json`: `hub_url`, `hub_token`, `jira.base_url`, `jira.auth` (`basic` с `user`/`password`
или `bearer` с `token`), `verify_ssl`, `ca_bundle`, `timeout`, `poll_seconds`, необязательный `auto_update` (по
умолчанию `true`). `hub_url` вида `http://…` или `https://…` SHALL означать HTTP-транспорт, вида
`folder:<абсолютный путь>` — транспорт через общую папку. Всё остальное (id полей, типы, карта переходов, типы связей,
доска, темп) агент SHALL получать от хаба при старте и при изменении `settings_version`. Учётные данные Jira MUST NOT
отправляться в хаб и MUST NOT попадать в журнал, логи и общую папку. Команды `agent.py`: `run` (по умолчанию), `check`,
`selftest`, `prune`.
#### Scenario: Проверка без записи
- **WHEN** `python3 agent.py check`
- **THEN** агент печатает доступность хаба (версия настроек) и Jira (`GET /myself`), в Jira ничего не пишет
#### Scenario: Проверка через папку
- **WHEN** `hub_url = "folder:/Volumes/My Shared Files/RSHB/jira-bridge"` и реле хаба работает
- **THEN** `check` печатает «хаб: доступен через папку /Volumes/My Shared Files/RSHB/jira-bridge, настройки v<N>, проект
<KEY>»; если каталога нет или реле не ответило за 10 с — строка с «НЕДОСТУПЕН» и причиной, код выхода 1
#### Scenario: Самопроверка новой версии
- **WHEN** `python3 agent.py selftest`, хаб и Jira доступны
- **THEN** печатается «jira-agent v<N>» и строки `check`; код 0; блокировка журнала не берётся, в Jira ничего не пишется;
если хаб или Jira недоступны — код 3
### Requirement: Доставка и запуск
README агента SHALL описывать: первую установку (копирование `agent.py`, `run.py` и `agent.example.json` через общую
папку в локальный каталог виртуалки, заполнение `agent.json` с выбором транспорта в `hub_url`, `check`), запуск
`python3 run.py` в терминале и через `nohup` с логом, опциональный launchd-plist с `run.py`, самообновление и
выключатель `auto_update`, ручное обновление `run.py`, `prune` (только при остановленном агенте, иначе откажет).
Версия агента SHALL быть в константе `AGENT_VERSION` файла (5 — самообновление) и в каждом `claim` и отчёте; хаб
предупреждает в `agent_status`, если версия ниже минимальной из настроек.
#### Scenario: Устаревший агент
- **WHEN** агент версии 1 выходит на связь, а в настройках `min_agent_version = 2`
- **THEN** `agent_status.warning` = «обновите агента», команды выдаются по-прежнему
## ADDED Requirements
### Requirement: Самообновление агента
Агент SHALL передавать в каждом `claim` `update_state` (содержимое `update.json` рядом с `agent.py` или `null`) и
`launcher_version` (из переменной окружения `JIRA_AGENT_LAUNCHER` или `null`). Проверять обновление агент SHALL только
после ответа `claim` с `command = null`, без сигнала остановки, при `auto_update ≠ false` и
`agent_release.version > AGENT_VERSION`. Версию с теми же `to_version` и `sha256`, что в `update.json` в состоянии
`rejected` или `rolled_back`, агент MUST NOT ставить снова. Без `JIRA_AGENT_LAUNCHER` агент MUST NOT выходить для
обновления: он пишет в лог «доступна v<N>; для самообновления запускайте через run.py» не чаще раза в час.
Подготовка версии SHALL идти по шагам: `GET /agent/release`; совпадение `version`/`sha256` ответа с `agent_release`
(иначе временный сбой); sha256 от `content` в UTF-8 равен `sha256` (иначе `rejected`); `content` компилируется и
содержит `AGENT_VERSION = <version>` (иначе `rejected`); запись `agent.py.new` атомарно; `python3 agent.py.new selftest`
с таймаутом 120 с — код 0 годен, код 3 или таймаут — временный сбой, иной код — `rejected` с последними 500 символами
вывода. Годную версию агент SHALL отметить в `update.json` состоянием `staged` и завершить `run` с кодом 75. При
`rejected` и временном сбое `agent.py.new` SHALL удаляться, агент продолжает работу; при `rejected` пишется
`update.json`; повтор после временного сбоя — не раньше чем через 5 минут. Первый успешный `claim` версии, для которой
`update.json` = `trial` с `to_version = AGENT_VERSION`, SHALL записывать состояние `ok`. `update.json` пишется атомарно
(временный файл и переименование).
#### Scenario: Обновление в паузе
- **WHEN** агент v5 под запускателем получил `claim` без команды и `agent_release = {version: 6, sha256: S}`, файл
проходит проверки и `selftest`
- **THEN** рядом лежит `agent.py.new`, `update.json` = `{state: staged, from_version: 5, to_version: 6, sha256: S}`,
процесс завершается с кодом 75; журнал не изменился
#### Scenario: Команда в очереди
- **WHEN** опубликована v6, а `claim` агента v5 вернул команду
- **THEN** агент выполняет команду и отчитывается; к обновлению переходит только после `claim` без команды
#### Scenario: Неверный sha256
- **WHEN** `GET /agent/release` вернул `content`, sha256 которого не равен `sha256`
- **THEN** `agent.py.new` не остаётся, `update.json` = `rejected` «sha256 не сходится»; следующий `claim` несёт этот
`update_state`; агент v5 работает дальше и v6 с тем же sha256 больше не скачивает
#### Scenario: Хаб недоступен во время selftest
- **WHEN** `selftest` новой версии вернул код 3
- **THEN** `agent.py.new` удалён, `update.json` не изменён, следующая попытка — не раньше чем через 5 минут
#### Scenario: Запуск без запускателя
- **WHEN** агент v5 запущен `python3 agent.py`, опубликована v6
- **THEN** агент не скачивает файл и не выходит; в логе раз в час «доступна v6; для самообновления запускайте через
run.py»; `claim` несёт `launcher_version = null`
#### Scenario: Подтверждение новой версии
- **WHEN** запускатель запустил v6, `update.json` = `trial` с `to_version = 6`, первый `claim` v6 успешен
- **THEN** `update.json` = `ok`; лог «обновление до v6 подтверждено»
### Requirement: Запускатель run.py
`python3 run.py [аргументы]` SHALL работать с файлами в каталоге самого `run.py` (`agent.py`, `agent.py.new`,
`agent.py.prev`, `update.json`) и запускать дочерним процессом `agent.py run` с переданными аргументами, переменной
окружения `JIRA_AGENT_LAUNCHER = LAUNCHER_VERSION` и унаследованными stdout/stderr. Запускатель SHALL брать
неблокирующую блокировку `run.py.lock`; занята — «запускатель уже работает», код 1. SIGINT при живом дочернем процессе
запускатель SHALL только запоминать как остановку человеком (Ctrl-C получает и агент), без дочернего процесса (пауза
перед перезапуском) — завершаться; SIGTERM — пересылать дочернему процессу. После сигнала остановки запускатель MUST NOT
запускать агента снова.
По коду выхода дочернего процесса:
- `0`, `130` или завершение по SIGINT после Ctrl-C (или после SIGTERM запускателю) — запускатель завершается с тем же
кодом; те же коды без сигнала запускателю — падение;
- `75` — при `update.json` = `staged` и совпадении sha256 `agent.py.new`: копия `agent.py` в `agent.py.prev`, атомарная
замена `agent.py` на `agent.py.new`, `update.json` = `trial`, запуск новой версии; иначе `agent.py.new` удаляется,
`update.json` = `rejected` «файл изменился после проверки», запуск текущей версии; если перед этим был Ctrl-C —
после подмены запускатель завершается, пробный период начнётся при следующем старте;
- иной код при `update.json` = `trial` — откат; иначе — перезапуск той же версии с паузой 5 с, удваиваемой до 300 с
при каждом следующем падении и сбрасываемой после 10 минут работы.
Пока `update.json` = `trial`, запускатель SHALL ждать состояния `ok` не дольше 10 минут; по истечении — SIGTERM, через
60 с SIGKILL и откат. Откат: `agent.py.prev` возвращается на место `agent.py`, `update.json` = `rolled_back` с причиной
(«код <N> через <S> с» или «не вышла на связь за 10 мин»), прежняя версия запускается без паузы. Файл `agent.py` MUST
существовать на каждом шаге подмены. При старте запускатель SHALL выполнять подмену, если `update.json` = `staged` и `agent.py.new` есть, и удалять
`agent.py.new` без состояния `staged`.
#### Scenario: Подмена версии
- **WHEN** дочерний v5 вышел с кодом 75, `update.json` = `staged` для v6, sha256 `agent.py.new` совпадает
- **THEN** `agent.py.prev` — прежний v5, `agent.py` — v6, `update.json` = `trial`, запущен v6
#### Scenario: Откат при падении на старте
- **WHEN** `update.json` = `trial` для v7, дочерний процесс вышел с кодом 1 через 3 с
- **THEN** `agent.py` — снова v6 (из `agent.py.prev`), `update.json` = `{state: rolled_back, to_version: 7, error: "код 1
через 3 с"}`, запущен v6; v6 видит `rolled_back` для v7 с тем же sha256 и v7 больше не ставит
#### Scenario: Новая версия не выходит на связь
- **WHEN** `update.json` = `trial`, дочерний процесс работает, но за 10 минут `ok` не записан
- **THEN** запускатель посылает SIGTERM, через 60 с при необходимости SIGKILL, делает откат с причиной «не вышла на связь
за 10 мин»
#### Scenario: Новая версия сама вышла с кодом 0
- **WHEN** `update.json` = `trial` для v7, дочерний процесс вышел с кодом 0, сигналов запускатель не получал
- **THEN** откат на v6 с причиной «код 0 через <S> с», как при падении
#### Scenario: Падение после подтверждения
- **WHEN** `update.json` = `ok`, агент дважды подряд упал с кодом 1, проработав меньше минуты
- **THEN** запускатель перезапускает ту же версию через 5 с, затем через 10 с; отката нет
#### Scenario: Остановка в терминале
- **WHEN** `python3 run.py` работает в терминале, нажат Ctrl-C
- **THEN** агент доделывает команду и выходит с кодом 0, запускатель завершается с кодом 0; повторный Ctrl-C — агент
выходит с 130, запускатель тоже
#### Scenario: Второй запускатель
- **WHEN** `run.py` работает, и запускается ещё один `run.py` в том же каталоге
- **THEN** второй пишет «запускатель уже работает» и завершается с кодом 1, агента не запускает