Andrey Epifancev f924153596 docs(openspec): архив change bug-and-attachments (#43)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 10:10:59 +04:00

20 KiB
Raw Blame History

Context

Хаб main 68de173 (после #44), агент main 7d90c9b (v10). Агент на виртуалке банка ходит в хаб через общую папку UTM (hub_url = folder:…), HTTP-транспорт тоже поддерживается.

Найдено при чтении кода (15.10.2026):

  • Реле папки (relay.py) отдаёт тело ответа строкой UTF-8 (decode("utf-8", "replace")), агент в HTTP-режиме понимает только JSON (Http.call). Двоичный ответ не пройдёт ни там, ни там; JSON со строкой base64 пройдёт обоими путями. Прецедент — GET /agent/release (content строкой, sha256 сверяет агент).
  • Предел тела запроса хаба MAX_BODY = 16 МБ (413), в реле предела нет; ответы не ограничены.
  • command.params (JSON) уходит в каждый claim и каждую довыдачу (до 3), копируется при retry, по нему ищутся команды задачи (params LIKE '%"KEY"%'), агент пишет params[:200] в лог — содержимому файла там не место.
  • issue_types в настройках — {task, epic}; issue_type_ref агента для неизвестного типа отдаёт {"name": None} — Jira ответит 400 без понятной причины. Зеркало хранит имя типа Jira (issue.issue_type), id типа не хранит; веб показывает тип как есть, «Ошибка» станет обычной карточкой без доработок.
  • Подстановка new:<n>, зависимость от ensure_issue и хвостовой refresh работают для любого типа с полем key в SPEC.keys. Старый агент на неизвестный тип отвечает failed «неизвестный тип команды» и не падает.
  • Подтверждения по состоянию у новых типов нет по умолчанию (check_confirmed → False).

Goals / Non-Goals

Goals: одним пакетом из чата завести «Ошибку», поставить в спринт и приложить локальный файл; повтор пакета не создаёт второй задачи и второго вложения; транспорт и реле не меняются.

Non-Goals: см. proposal (веб-загрузка, список вложений в зеркале, удаление вложений, поля по умолчанию, гейтинг по версии агента, файлы больше 10 МБ).

Decisions

Тип «Ошибка»

issue_types.bug = {name: "Ошибка", id: null} ставится на живом хабе settings_set; id впишем, когда агент создаст первую «Ошибку» (Jira вернёт тип в задаче). Хаб проверяет наличие настройки при постановке (быстрая ошибка в чате), агент — при выполнении (настройку могли убрать между постановкой и выдачей).

Хранилище файлов

Модуль files.py хаба: put(content_bytes) -> (sha, size), path(sha), exists(sha), size(sha), sweep(conn, now). Каталог <home>/files/, имя — sha256, без имени вложения (одно содержимое может уходить под разными именами). Запись: временный .<sha>.tmp в том же каталоге, fsync, os.replace. Уборка — из tick не чаще раза в сутки: файлы старше 30 дней по mtime; ссылки — params команд ensure_attachment в queued/claimed (поиск json_extract(params, '$.file')). Повторная загрузка того же файла обновляет mtime (os.utime), чтобы свежая постановка не потеряла файл в уборке.

Команда и выдача

SPEC["ensure_attachment"] = {required: [key, file, filename], optional: [], keys: [key], hints: {file: "sha256 из POST /files; в MCP — path к локальному файлу", filename: "имя вложения"}}. Проверки типа значения — общие; формат file, наличие файла и filename — в проверке постановки, которой нужен каталог файлов (не в чистой validate, а в queue.create_batch с доступом к хранилищу). size дописывается при выдаче (claim) — там же, где substitute подставляет временные ключи; в базе params без size, поэтому retry и GET /agent/commands/{id} (восстановление из журнала — тоже с size) работают одинаково.

MCP

batch_create до POST /batches обходит команды: у ensure_attachment с path читает файл (не больше 10 МБ, иначе ошибка до загрузки), POST /files, подставляет file и filename (если не задан — Path(path).name), удаляет path. Ошибки чтения — текст инструмента с путём, пакет не ставится. Загрузка без последующей постановки (422 из-за другой команды) оставляет файл в хранилище — его уберёт уборка.

Агент v11

do_ensure_attachment: GET /issue/{key}?fields=attachment → совпадение по filename и size → done (existed: true); иначе hub.file(sha) → проверка sha256 и длины → Jira.upload(key, filename, data): multipart/form-data, граница — uuid4().hex, часть file с Content-Disposition: form-data; name="file"; filename="<имя UTF-8>"; filename*=UTF-8''<percent-encoded>, Content-Type: application/octet-stream. Таймаут загрузки — max(timeout, 120) с. Ответ Jira — список вложений, берётся id первого. Содержимое файла в журнал и лог не пишется. Заодно M-1 и M-2 из aep/jira-agent#30 в comments_of/comment_state.

Выкатка

  1. Merge PR хаба, git pull на Mac mini, перезапуск хаба (launchd) и MCP; make web не нужен (фронт не меняется).
  2. settings_set issue_types = {task…, epic…, bug: {name: "Ошибка", id: null}}.
  3. Merge PR агента, jira-hub agent publish …/agent.py (v11), агент обновится сам; min_agent_version = 11.
  4. Живая проверка: пакет «Ошибка в бэклог + вложение с кириллическим именем» на тестовой задаче, повтор пакета — без дублей; тестовую задачу владелец удаляет в Jira; id типа «Ошибка» вписать в issue_types.bug.id.

Risks / Trade-offs

  • base64 даёт +33 % к размеру, файл 10 МБ — около 13,4 МБ JSON в одном файле общей папки; VirtioFS это переносит (круг папки < 1 с на малых файлах, на больших — секунды), таймаут вызова агента 60 с с запасом.
  • Имя файла с кириллицей в multipart: Jira Server (commons-fileupload) читает filename в кодировке запроса; отправляем UTF-8 и filename*. Проверяется только живой загрузкой (шаг 4 выкатки); если имя исказится — следующей версией агента.
  • Предел вложений Jira банка неизвестен (по умолчанию 10 МБ); больший предел банка не используется, меньший даст 413 от Jira — failed с текстом.
  • Идемпотентность по filename + size: другой файл того же размера и с тем же именем будет принят за уже приложенный.

Decisions made without discussion

  1. 15.10.2026 — предел файла 10 МБ (после декодирования). Обоснование: стандартный предел вложений Jira, запас под 16 МБ тела запроса хаба с base64. Цена ошибки: файл 10–16 МБ нельзя приложить через мост — поднять предел одной константой.
  2. 15.10.2026 — хранение файлов 30 дней с уборкой, кроме файлов активных команд. Обоснование: как backup_keep (30); retry старой команды после уборки даёт понятный 422 «файл не загружен». Цена: повторная загрузка файла из чата.
  3. 15.10.2026 — пустой файл отвергается (422 empty_file). Обоснование: пустое вложение — почти всегда ошибка пути или выгрузки. Цена: нельзя приложить пустой файл намеренно.
  4. 15.10.2026 — вложение с тем же именем, но другим размером загружается вторым вложением, старое не удаляется. Обоснование: команда только добавляет недостающее, удаление в Jira — необратимая операция. Цена: два вложения с одним именем, лишнее владелец удаляет руками.
  5. 15.10.2026 — size дописывается хабом при выдаче, а не хранится в params. Обоснование: retry копирует params и проходит обычную проверку; клиент не может прислать неверный размер. Цена: выдача читает размер файла с диска.
  6. 15.10.2026 — наличие настройки issue_types.bug проверяют и хаб (при постановке), и агент (при выполнении). Обоснование: быстрая ошибка в чате; настройку могли изменить между постановкой и выдачей. Цена: две проверки одного.
  7. 15.10.2026 — проверка наличия файла и формата file — в queue.create_batch, а не в чистой commands.validate. Обоснование: validate не знает каталог данных. Цена: два места проверки параметров одной команды.
  8. 15.10.2026 — в change включены M-1 и M-2 из aep/jira-agent#30 и R-1 из aep/jira-hub#46 (по порядку, согласованному владельцем 15.10.2026). M-3, M-4 из jira-agent#30 остаются в follow-up. Цена: change немного шире двух issue.
  9. 15.10.2026 — issue_type вне списка — 422 «команда N: issue_type — task, epic или bug»; init добавляет bug, если ключ issue_type_bug или issue_type_bug_id есть в конфиге даже со значением null (тогда постановка bug — 422, как без настройки). Обоснование: спека задаёт условие по наличию ключей, не по значению. Цена: нет.
  10. 15.10.2026 — функции files принимают каталог хранилища первым параметром (put(root, data), path(root, sha), exists, size, read, sweep(conn, root, now)), каталог — Hub.files_dir (<каталог базы>/files), а не paths.home(). Обоснование: тесты поднимают хаб во временном каталоге без JIRA_HUB_HOME (как release_dir). Цена: лишний параметр в вызовах из queue и api.
  11. 15.10.2026 — временный файл записи — tempfile.mkstemp в files/ (.<sha>.<случайное>.tmp, права 0600), а не фиксированное .<sha>.tmp. Обоснование: две одновременные загрузки одного содержимого (ThreadingHTTPServer) не пишут в один временный файл и не ломают друг другу os.replace. Цена: брошенные после сбоя временные файлы убирает уборка общим правилом 30 дней.
  12. 15.10.2026 — files.path/exists/size/read принимают только sha из 64 символов [0-9a-f], иначе «файла нет» (path — ValueError). Обоснование: APFS не различает регистр — BA78… нашёл бы ba78…; .. и / не строят путь вне files/. Цена: нет.
  13. 15.10.2026 — повторная загрузка: целая копия (тот же размер и sha256 содержимого на диске) — только os.utime; оборванная (другой размер) или испорченная (тот же размер, другой sha256 — M-2 финального ревью) — переписывается. Обоснование: имя — sha256 содержимого; повторная загрузка из чата должна лечить испорченную копию. Цена: чтение копии (до 10 МБ) при каждой повторной загрузке.
  14. 15.10.2026 — POST /files отбрасывает пробельные символы (переносы строк) в content до строгой проверки base64. Обоснование: GNU base64 и многие библиотеки переносят строку через 76 символов; спека говорит только «не base64 — 400». Цена: строка с пробелами внутри тоже принимается.
  15. 15.10.2026 — время последней уборки — в памяти процесса (Hub.last_sweep); первая уборка — на первом тике после старта (через 60 с). Обоснование: без новой настройки и файла-метки, уборка дешёвая. Цена: при нескольких перезапусках хаба в сутки уборок больше одной (спека уточнена: «в пределах работы процесса», рулинг R-1).
  16. 15.10.2026 — GET /agent/files/{id} с id не из 64 hex нижнего регистра — 404 no_file, а не общий not_found. Обоснование: для агента это тот же случай «файла нет». Цена: нет.
  17. 15.10.2026 — тексты 422 без формулировки в спеке: «команда N: file — sha256 из POST /files (64 символа 0-9a-f)», «команда N: filename пустой» (только пробелы; пустая строка — общее «нет filename; ожидаются …»), «команда N: filename без / и \ — имя вложения, а не путь». Порядок проверок: формат file, наличие файла, filename. Цена: формулировки правятся одной строкой.
  18. 15.10.2026 — queue.create_batch, queue.retry_command и queue.claim получают files_dir параметром (по умолчанию None: вложение не ставится — «файл не загружен», при выдаче size = null); api передаёт Hub.files_dir; GET /agent/commands/{id} дописывает size через queue.deliver_params. Обоснование: queue остаётся чистой логикой над соединением (решение 10). Цена: новый вызов без files_dir молча отвергает вложения.
  19. 15.10.2026 — MCP batch_create с path: ~ раскрывается; относительный путь — «нужен абсолютный путь: »; пустой файл отвергается до загрузки — «пустой файл: »; прочие тексты — «не файл: », «нет прав на чтение: », «файл больше 10 МБ ( байт): », «файл не прочитан (<причина>): »; сначала читаются все файлы пакета, затем загружаются — ошибка любого файла оставляет хаб без загрузок. Обоснование: cwd сервера MCP неочевиден; спека дословно задаёт только «файл не найден» и «либо path, либо file». Цена: пакет с N файлами держит их в памяти MCP до загрузки (≤ N × 10 МБ).
  20. 16.10.2026 — хаб при постановке отвергает filename с " и управляющими символами (код < 32 и 127, в том числе CR и LF) — «команда N: filename без кавычек и управляющих символов (CR, LF и др.)» (рулинг R-2). Обоснование: такие имена ломают заголовок части multipart/form-data у агента; кодирование имени в агенте остаётся запасной защитой. Цена: файл с кавычкой в имени прикладывается под другим именем (filename в команде).
  21. 16.10.2026 — текст отчёта агента при повреждённом в пути файле («результат неизвестен (обрыв связи с хабом)…») не меняется (рулинг R-3): общий путь повтора Runner.execute. Цена: неточная формулировка в редком случае — в Jira при этом ничего не записано.
  22. 16.10.2026 — агент без записи issue_types.<тип> (в том числе task и epic) отвечает failed «нет настройки issue_types.<тип>» без вызова Jira вместо {"name": null} (рулинг R-4, следует из спеки agent). Цена: нет — на живом хабе настройки task и epic есть; отмечено в PR.
  23. 16.10.2026 — план агента (5 задач) исполнен параллельно фоновым исполнителем на sonnet по executing-plans, план хаба — в сессии; финальное ревью обеих веток — opus (рулинг R-5). Цена: два контекста вместо одного.