Andrey Epifancev d327a0488d docs(openspec): change bug-and-attachments — тип «Ошибка» и вложения (#43)
Дельты commands, hub-api, mcp, data-model, agent: issue_type bug с настройкой
issue_types.bug, хранилище файлов хаба (POST /files, GET /agent/files/{sha256}),
команда ensure_attachment, path в MCP batch_create, агент v11; заодно R-1
(#46) и M-1/M-2 из aep/jira-agent#30.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 23:42:21 +04:00

11 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.