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

14 KiB
Raw Blame History

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, проект »; если каталога нет или реле не ответило за 10 с — строка с «НЕДОСТУПЕН» и причиной, код выхода 1

Scenario: Самопроверка новой версии

  • WHEN python3 agent.py selftest, хаб и Jira доступны
  • THEN печатается «jira-agent v» и строки 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; для самообновления запускайте через 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 с причиной («код через с» или «не вышла на связь за 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 через с», как при падении

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, агента не запускает