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

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

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