dsh-qa-integrations
Персональные интеграции для @yadsh/dsh-qa-surface. Плагин подключает Bitrix24 через URL входящего вебхука, GitLab через personal access token, TeamCity через access token, Jira и Confluence — через API-токен Atlassian, Test IT — через API-токен системы управления тестированием, а Weblate — через API-токен платформы локализации, после чего даёт агенту набор read-only инструментов, ограниченный и правами подключения, и персональной политикой пользователя; единственное исключение — комментарий в таймлайн Bitrix24, который монтируется только явным флагом оператора. У всех семи интеграций подключение может работать не только от личного токена, но и от сервисного токена развёртывания — общего read-only аккаунта с жёстким потолком режима (раздел «Сервисные токены»).
Плагин показывает подключения двумя монтированиями одного и того же набора карточек: отдельным разделом «Интеграции» в пользовательских настройках QA и секцией конфигурации самого плагина на панели «Плагины» оригинального DSH (слот plugins.bundle.config, ключ — имя пакета). Панель рисуется клиентом и не читает loopback-only каталог Host settings: в установленном @deepseek-ai/dsh-client-ui-plugin-manager 0.1.7-rc.2 ветки isLoopback нет вовсе, а единственный loopback-выбор на стороне настроек выбирает режим записи — host или memory — и не зависит от того, на каком слоте висит карточка. Поэтому секция доступна и в LAN-браузере, где тот каталог намеренно отключён. Слот выбран по AGENTS.md («Choosing the registration point»): карточка конфигурации плагина сидит на панели «Плагины», а не в диалоге настроек. Оболочку карточка не рисует: на ряду панели «Плагины» страницу, заголовок строки, id строки и описание выдаёт хост, а карточка монтирует только тело — own shell остаётся за settings.section и settings.plugins.tab (решение владельца от 01.10, влитое в контракт #684). Кольцо фокуса строится из хозяйских токенов --dsw-focus-ring-width / --dsw-focus-ring-color с fallback у каждой половины. Клик-обход живого не-loopback стенда — пункт приёмки переноса. Она читает аккаунт через клиентский сервис qaUserSession плагина @yadsh/dsh-qa-surface: без входа в QA ничего не показывает и не делает запросов. Токен вводится один раз, шифруется на Host и никогда не возвращается браузеру. Выбор пользователя, integration id, secret id или токена отсутствует в model-visible схемах: principal берётся из DSH-сессии, а Bitrix user id — из сохранённой записи интеграции.
Операторская карточка настроек
Кроме двух пользовательских монтирований, плагин рисует операторскую карточку по своей конфигурации — секцией конфигурации своей строки профиля на панели «Плагины» (слот plugins.row.config, ключ @yadsh/dsh-qa-integrations#qa-integrations), под секцией аккаунта. На хосте 0.1.7 роль settings namespace играет id строки профиля (qa-integrations), а доступными для браузера становятся те поля, чей узел схемы помечен .volatile(); карточка берёт форму этого входа через ctx.configForms и появляется вместе с рядом, не дожидаясь Remote-описания развёртывания. Ключ нового места собран из того же id строки и имени пакета, поэтому переехало только место рендера: значение, сохранённое до переноса, читается после. Обе карточки живут на панели «Плагины», куда не нужен loopback-only каталог Host settings, и каждая монтирует только тело — рамку, заголовок и раскрытие рядом рисует сама страница плагина. Операторская карточка — третья поверхность того же бандла, и она не требует ни входа в QA, ни Remote-описания развёртывания: оператор включается и настраивает плагин из этой карточки, даже когда плагин ещё выключен.
Карточка читается сверху вниз: у каждой секции в свёрнутом виде строка состояния (включён ли провайдер, сколько подключений настроено, сколько возможностей открыто), а внутри — четыре подписанных блока: «Провайдер» (выключатели провайдера), «Подключение» (инстансы, сайты, адреса и сетевые политики во всю ширину), «Что доступно агенту» (чек-лист возможностей, флажок перед подписью) и свёрнутые «Ограничения и повторы» (потолки, таймауты, повторы при ошибках). Подпись поля связана с самим полем: клик по названию ставит курсор в него. Переключатель, до которого сервисный токен развёртывания не дотягивается целиком или частично, говорит об этом прямо в чек-листе («с сервисным токеном недоступно: требуется личный аккаунт»): сам по себе включённый флажок ничего оператору не объясняет, а пользователь с отказом читает его как сломанную интеграцию. Заметка появляется только при включённых сервисных доступах и снимается самим переключателем по его пути в конфигурации: копии имени флага в карточке нет, опечатать в ней нечего. tests/client/operator-service-reach.test.ts сверяет таблицу заметок с классификацией каждого каталога, а tests/client/operator-card.test.tsx считает заметки в отрисованной карточке — карточка не сможет разойтись с тем, что провайдер на самом деле разрешает общему аккаунту.
Карточка покрывает всю разрешённую резолверами конфигурацию: общие ручки (enabled, таймаут, потолки ответа и аудита, пути хранилища и мастер-ключа, суффиксы порталов Bitrix24), у каждого провайдера — выключатель, возможности, инстансы/сайты (строки id/название/адрес) и лимиты, у TeamCity — адрес сервера и сетевую политику, у Jira — псевдонимы полей, плюс сервисные доступы (профили с ресурсными границами и deny-политикой) и замены встроенных подсказок получения токена (credentialHelp). Половины GitLab CI — ciMetadataRead и ciLogsRead — это в карточке два переключателя, а не один ciRead: резолвер отвечает половинами отдельно и складывает в них старое ciRead, только пока ни одна из них не названа явно. Единая ручка этого состояния выразить не могла: половина, выключенная отдельно от другой, читалась «включено», а правка по старому имени переставала действовать, как только вторая половина названа. Поэтому карточка читает каждую половину в том же порядке, что и резолвер, — половина ?? ciRead ?? значение по умолчанию. Каждое поле помечает, что пользовательский слой namespace переопределяет композиционную строку профиля, одна кнопка очищает слой целиком. Хост проверяет правку по схеме и отвергает то, что схема не позволяет; кросс-полевые ограничения (политика хостов TeamCity, дубликат id в списке инстансов) схема выразить не может, и их держат резолверы: значение, которое они не принимают, остаётся в документе профиля, а запущенный сервис сохраняет прежнее состояние и пишет config.rejected в лог.
Правка применяется к запущенному сервису сразу: Loader коммитит её в volatile-ссылки самой строки и сообщает об этом её фибре (loader/volatile-update), брокер перепривязывается к свежему набору провайдеров, а монтирование инструментов следует за выключателем плагина и единственным флагом записи. Хранилище подключений и мастер-ключ — сознательное исключение: подключения и зашифрованные секреты живут по путям загрузки, поэтому их правка предупреждает в логе и вступает в силу при следующем рестарте Host. Без отдаваемого namespace карточка объясняет это одной строкой вместо пустой секции, а плагин грузится от своей композиционной строки — настройки профиля ему не нужны, чтобы работать.
Структура
Один каталог — одна интеграция. Всё, что знает про Bitrix24, GitLab, TeamCity, Jira, Confluence, Test IT и Weblate, лежит в своём каталоге src/providers/<id>/: каталог возможностей и операций, построение запросов, HTTP-граница, срез конфига и тулы. У TeamCity отдельно лежат locator-синтаксис, обработка логов, политика артефактов и сеть; у Jira — JQL и ADF; у Confluence — CQL и ADF; у Test IT — политика видов вложений и бюджет их чтения; у Weblate — сборка поискового синтаксиса q. Общий слой — брокер, репозиторий, секреты, реестр сервисных токенов (src/service-credentials/), обвязка тулов — не знает ни одной интеграции по имени: capability это свободная строка, а providers/contract.ts описывает, что должен уметь провайдер. Порядок добавления следующей интеграции и правила гейта пакета — в src/providers/README.md.
Инструменты
CRM:
| Tool | Что делает |
|---|
bitrix_search_crm | Поиск лидов, сделок, контактов, компаний, счетов и смарт-процессов: по названию, стадии, воронке, свежести, открытости (только сделки), ответственный; порядок сортировки зафиксирован, чтобы страницы не дублировались |
bitrix_get_crm_item | Полная карточка элемента CRM, включая пользовательские поля и коммуникации |
bitrix_get_crm_fields | Схема полей типа сущности, включая UF_CRM_* портала |
bitrix_get_crm_funnels | Воронки (категории) типа сущности |
bitrix_get_crm_statuses | Расшифровка стадий и справочников (STATUS, DEAL_STAGE, DEAL_STAGE_<id>, SOURCE, …) |
bitrix_get_crm_activities | Дела CRM: звонки, встречи, письма, задачи; просрочки и незавершённые |
bitrix_get_crm_activity | Одно дело целиком, включая описание |
bitrix_get_crm_timeline | Комментарии таймлайна лида, сделки, контакта, компании |
bitrix_get_crm_stage_history | История движения по стадиям: сколько висело, куда возвращали |
bitrix_get_crm_product_rows | Товарные позиции: что продаём, количество, цена, скидки |
bitrix_find_crm_duplicates | Поиск дублей клиента по телефону или e-mail |
bitrix_get_crm_requisites | Реквизиты контакта или компании: ИНН, КПП, адрес, банк |
bitrix_get_call_transcript | Готовая AI-расшифровка звонка по делу CRM |
Сотрудники и структура:
| Tool | Что делает |
|---|
bitrix_get_current_user | Чей вебхук подключён |
bitrix_search_users | Поиск сотрудников по имени, e-mail, подразделению |
bitrix_get_departments | Подразделения, родительский отдел, руководитель |
bitrix_get_user_fields | Какие поля сотрудника доступны с этим вебхуком |
Чаты и открытые линии:
| Tool | Что делает |
|---|
bitrix_search_chats | Поиск чатов по названию и участникам |
bitrix_get_chat_messages | Последние сообщения чата |
bitrix_search_chat_messages | Поиск по тексту и датам внутри одного чата |
bitrix_get_recent_chats | Последние диалоги пользователя, счётчики непрочитанного |
bitrix_search_chat_users | Поиск сотрудника как контакта чата: статус, телефоны |
bitrix_find_chat | Чат, привязанный к объекту: обсуждение сделки, чат задачи, событие календаря, чат группы |
bitrix_get_chat_participants | Кто участвует в чате |
bitrix_get_chat_user_data | Профили участников: имя, должность, телефоны, присутствие |
bitrix_get_openline_dialog | Диалог открытой линии: участники, связь с CRM |
bitrix_get_openline_history | История переписки с клиентом в открытой линии |
Задачи, календарь, Диск:
| Tool | Что делает |
|---|
bitrix_search_tasks | Задачи по названию, ответственному, группе, дедлайну |
bitrix_get_task | Карточка задачи целиком, включая ufCrmTask |
bitrix_get_task_history | История изменений задачи: что, когда и кем |
bitrix_get_task_results | Результаты работы по задаче |
bitrix_get_task_elapsed_time | Затраченное время: кто сколько залогировал |
bitrix_get_calendar_events | События календаря сотрудника, группы или компании |
bitrix_get_calendar_accessibility | Занятость сотрудников, чтобы предложить время встречи |
bitrix_search_files | Поиск по Диску, включая текст внутри документов |
bitrix_get_file | Метаданные и ссылка на файл Диска |
bitrix_get_drives | Доступные диски и их id |
bitrix_get_storage_items | Содержимое корня диска |
bitrix_get_folder_items | Содержимое папки Диска |
Запись (по умолчанию выключена оператором bitrix24.crmCommentWrite):
| Tool | Что делает |
|---|
bitrix_add_crm_timeline_comment | Один комментарий в таймлайн лида, сделки, контакта или компании; единственная операция записи во всём плагине |
Списочные инструменты отвечают единым конвертом { items, pagination }: у модели одна форма ответа вместо шести разных у REST, а курсор следующей страницы не теряется. Методы, возвращающие словари по id (история открытой линии, занятость), спроецированы в упорядоченные массивы.
Инструменты GitLab
| Tool | Что делает |
|---|
gitlab_connection_get | К какому инстансу и каким пользователем подключён аккаунт |
gitlab_projects_list | Поиск проектов: путь, ветка по умолчанию, видимость |
gitlab_project_get | Карточка проекта: описание, namespace, даты |
gitlab_repository_tree | Содержимое каталогов на выбранном ref, постранично |
gitlab_repository_file_get | Текст файла с метаданными; бинарное и слишком большое — только метаданные |
gitlab_commits_list | Коммиты проекта, ветки или одного файла |
gitlab_commit_get | Один коммит: сообщение, автор, родители, объём изменений |
gitlab_compare | Сравнение двух refs: коммиты и изменённые файлы |
gitlab_search | Поиск по проектам, задачам, MR, коммитам, коду и комментариям |
gitlab_issues_list | Задачи по проекту, состоянию, автору, исполнителю, меткам, датам |
gitlab_issue_get | Карточка задачи: описание, веха, срок |
gitlab_issue_notes_list | Комментарии задачи, системные помечены |
gitlab_merge_requests_list | Merge requests по проекту, веткам, автору, ревьюеру, черновику |
gitlab_merge_request_get | Карточка MR: описание, ветки, статус слияния, конфликты |
gitlab_merge_request_changes_get | Изменённые файлы MR с диффами |
gitlab_merge_request_discussions_list | Обсуждения ревью и их разрешение |
gitlab_merge_request_approvals_get | Сколько одобрений нужно, сколько есть, кто одобрил |
gitlab_merge_request_pipelines_list | Пайплайны, привязанные к MR |
gitlab_pipelines_list | Пайплайны проекта по ветке, статусу, источнику, автору |
gitlab_pipeline_get | Один пайплайн: статус, длительности, покрытие |
|
Списочные инструменты GitLab отвечают тем же конвертом { items, pagination }; pagination.nextPage — это номер следующей страницы, а не подписанный курсор: состояние между вызовами не хранится, principal и права перепроверяются на каждом вызове.
Диффы (gitlab_compare, gitlab_merge_request_changes_get) отдают список файлов целиком, а текст диффов — в пределах общего бюджета символов; при обрезке ответ помечается diffTruncated. Логи CI обрезаются по лимиту стенда и дополнительно проходят через редакцию секретов: встроенная маскировка GitLab — фильтр, а не гарантия.
Инструменты TeamCity
| Tool | Что делает |
|---|
teamcity_connection_get | К какому серверу TeamCity и каким пользователем подключён аккаунт, версия сервера |
teamcity_projects | Проекты: поиск по id и названию, дочерние проекты, архивные |
teamcity_build_configs | Конфигурации сборки: id, название, проект, пауза |
teamcity_builds | Поиск сборок по проекту, конфигурации, ветке, статусу, состоянию и датам |
teamcity_build | Одна сборка: состояние, статус и его текст, агент, кто запустил, времена |
teamcity_build_changes | Изменения в VCS, попавшие в сборку: ревизия, автор, дата, комментарий |
teamcity_build_failures | Почему упало: упавшие тесты и проблемы сборки одним ответом |
teamcity_build_log | Фрагмент лога сборки: конец, начало или строки по поиску |
teamcity_queue | Очередь сборки: что ждёт агента и на какой ветке |
teamcity_investigations | Расследования падений: состояние, ответственный, разрешение |
teamcity_agents | Агенты сборки: подключён, включён, авторизован |
teamcity_artifacts | Артефакты сборки: имя, путь, размер, время изменения |
teamcity_artifact_text | Текст небольшого артефакта: отчёты тестов, лог-файлы |
Списочные инструменты TeamCity отвечают конвертом { items, pagination }, где pagination.returned — сколько строк вернулось, а pagination.hasMore — что сервер отдал не всё: инструмент никогда не ходит по nextHref сам, поэтому продолжение получается сужением фильтра, а не вторым запросом за спиной у модели. Лимит каждого списка задан спецификацией (сборки и очередь — 50, проекты, конфигурации, изменения, тесты, проблемы, расследования и агенты — 100, артефакты — 200) и не поднимается выше, сколько бы модель ни попросила.
Лог сборки скачивается не больше, чем разрешает стенд (maxLogBytes, по умолчанию 256 КиБ), чистится от управляющих последовательностей терминала, проходит через редакцию секретов и режется до запрошенного числа строк (maxLines, максимум 1000). Ответ различает два случая: truncated — окно не покрыло запрошенные строки, logTruncated — сам лог длиннее скачанного куска, и тогда в режиме tail это конец скачанного, а не конец сборки. Артефакты читаются только текстовые: архивы, образы, бинарники, документы и ключи (zip, jar, png, pdf, pem, …) отказываются по имени до запроса, а бинарное тело отвечает метаданными с binary: true вместо содержимого. Логи и артефакты описаны модели как недоверенные данные: это текст из внешней системы, а не инструкция.
Инструменты Jira
| Tool | Что делает |
|---|
jira_get_current_user | К какому сайту Jira и каким пользователем Atlassian подключён аккаунт |
jira_search_issues | Поиск задач: текст, проекты, типы, статусы и их категория, приоритеты, резолюции, компоненты, метки, версии (fix и affected), люди, даты, история поля, кастомные поля |
jira_get_issue | Карточка задачи: ключ, ссылка, проект, тип, статус, приоритет, люди, метки, компоненты, версии, резолюция, даты и описание |
jira_get_issue_comments | Комментарии задачи, от новых к старым; ограниченный комментарий помечен видимостью |
jira_get_issue_attachments | Метаданные вложений: имя файла, тип, размер, автор; файлы не скачиваются |
jira_get_available_transitions | Доступные переходы статуса и их обязательные поля; переход не выполняется |
jira_get_project | Карточка проекта: ключ, название, тип, руководитель, описание, ссылка |
jira_get_fields | Поля сайта: id, имя, признак кастомного поля, тип и JQL-имена |
Модель не получает JQL: фильтры в jira_search_issues типизированные, а строку запроса собирает и экранирует провайдер — значение с кавычкой или обратным слэшем остаётся значением и не может добавить условие. Поиск без единого фильтра отказывается: «все задачи сайта» — не вопрос, на который этот инструмент отвечает. Ответы идут по последнему обновлению (сначала свежие) и несут pagination.nextCursor — это continuation-токен самого Jira, состояние между вызовами не хранится, а страница не выходит за потолок стенда (defaultSearchLimit, maxSearchLimit) и за 100 строк, которые Jira отдаёт с полями. Комментарии Jira по-прежнему листаются позицией, поэтому их pagination — { startAt, returned, total, hasMore }.
Точное число результатов ищет не провайдер, а продукт: Server / Data Center считает поиск по позиции и отдаёт размер ответа, поэтому его pagination несёт total (и startAt, и следующий offset в nextCursor), а Atlassian Cloud на /search/jql числа не сообщает вовсе — там остаётся только nextCursor и isLast, и «сколько всего» превращается в вопрос к самому Jira, а не к этому пакету. Провайдер не выдумывает оценку: счётчика нет в ответе — нет и в конверте.
Словарь фильтров покрывает то, что спрашивают у корпоративной Jira, и каждый фильтр — это клауза, которую собирает jql.ts: проект, тип задачи, статус и категория статуса (To Do / In Progress / Done — «закрытые» одним фильтром, чем бы ни называл их workflow), приоритет, резолюция, компонент, метки (все перечисленные, а не «любая из»), версии — fixVersions и affectedVersions, включая «не проставлена» (fixVersionEmpty: true) и «проставлена» (false), исполнитель и автор, даты создания и обновления в обе стороны, история поля (history, см. ниже) и кастомные поля. Даты принимают и абсолютное значение (2026-08-01, ISO-таймстемп), и родной относительный токен Jira (-3w, -2d, -4h, -30m), а пара «с/по» — это createdAfter/createdBefore (границы включительные). Текст ищется двумя способами: match: "all" (по умолчанию) требует все слова, match: "phrase" — точную фразу; оба варианта бьются на отдельные клаузы text ~ "…", поэтому OR из фразы остаётся словом, а не оператором.
history — фильтр не по тому, что задача несёт сейчас, а по тому, что с ней делали: JQL-операторы WAS и CHANGED собираются из типизированной записи, { field: "status", op: "was", value: "In Progress" } отвечает «побывали в In Progress», { field: "status", op: "changed", after: "-2w" } — «двигали статус за две недели», { field: "assignee", op: "changed", value: "5b10…" } — «переназначали на вот этого человека». Поле ограничено списком status, assignee, reporter, priority, resolution, fixVersion — тем, у чего Jira ищет историю; кастомное поле здесь отказывается, для него есть customFields. Ключевые слова читаются по строгому набору: was требует значение и не принимает from, changed принимает from (откуда ушло) и value (куда пришло), by — кто двигал, дата — один день (on) или край окна (after/before), а день вместе с окном — противоречие. Значения экранируются как любое другое, поэтому история не распахивает запрос: клауза OR project = SECRET из value остаётся текстом внутри кавычек. Человек здесь — me или идентификатор, по которому фильтрует этот продукт, но не имя: справочник пользователей за этим фильтром не читается (в отличие от assignee/reporter, где имя разрешается провайдером), потому что идентификатор уже лежит в истории самой задачи — в changelog_summary. Неразборчивая запись отказывается целиком, а не собирается «из того, что удалось распарсить»: молча потерянная граница отвечала бы на другой вопрос, а её ответ выглядел бы как факт о задачах. Клауз — не больше трёх, и они, как остальные фильтры, соединяются через AND.
Кастомные поля фильтруются по id, который вернул jira_get_fields, или по алиасу, который объявил оператор стенда: customFields: [{ field: "product", value: "…" }], с match: "contains" для текстовых полей и empty: true/false для пустого/заполненного. Имя поля Jira не принимается намеренно — на одном сайте легко живут несколько полей с одинаковым именем, а какой из них «продукт», знает только развёртывание: соответствие «алиас → customfield_…» задаётся в конфиге оператора (jira.fieldAliases), поэтому в репозитории нет ни одного id конкретного инстанса, а каталог полей (jira_get_fields) отдаёт объявленные алиасы вместе со схемой. Неизвестное имя — отказ со списком алиасов, а не пустая страница. Люди принимаются как me, как идентификатор, по которому фильтрует эта Jira (accountId у Cloud, логин у Server / Data Center), или как имя: имя провайдер разрешает через справочник сайта (/rest/api/3/user/search или /rest/api/2/user/search, не больше десяти совпадений) и подставляет этот идентификатор в запрос. Имя, которое никого не нашло, и имя, которое нашли несколько человек, — это отказ с подсказкой, а не пустая страница: «нет задач» и «фильтр не понят» — разные ответы. Ответ справочника модели не показывается, он только определяет id, по которому построен запрос.
Описания и комментарии приходят в Atlassian Document Format и рендерятся в текст с сохранением структуры: заголовки, списки, код, ссылки, упоминания, таблицы и вложения-маркеры. Сырой JSON документа модели не отдаётся, а встроенные ссылки и медиа-узлы не загружаются. История изменений задачи читается отдельной группой include: ["changelog_summary"] (Jira отдаёт её только через expand=changelog): ответ несёт до 20 изменённых полей с автором и значениями «было/стало» и честно различает две обрезки — свою (truncated, упёрлись в 20 записей) и джировскую (groupsTruncated, Jira посчитала групп изменений больше, чем прислала). Кастомные поля называются по схеме сайта (jira_get_fields показывает её модель целиком): схема читается только после того, как задача ответила, и не кэшируется — один и тот же сайт отдаёт разный список полей двум пользователям с разными правами, а кэш на двоих был бы утечкой ради одного запроса. Тело задачи, комментария или кастомного поля ограничено maxTextChars (по умолчанию 20 000 символов); при обрезке ответ помечается descriptionTruncated или bodyTruncated. Текст из Jira — данные, а не инструкция.
Инструменты Confluence
| Tool | Что делает |
|---|
confluence_connection_get | К какому сайту Confluence и каким аккаунтом Atlassian подключён пользователь |
confluence_search | Поиск страниц по тексту, пространствам, меткам, автору и дате изменения |
confluence_get_page | Страница целиком: заголовок, пространство, родитель, версия, метки и текст |
confluence_get_page_comments | Комментарии страницы: подвал, inline или оба, с ответами и статусом резолва |
confluence_get_page_attachments | Метаданные вложений: имя, тип, размер, версия, автор, ссылки |
confluence_get_page_versions | История версий: номер, автор, дата и комментарий к версии |
confluence_get_space | Пространство: ключ, имя, тип, статус, описание |
confluence_list_spaces | Пространства, доступные аккаунту, с ключами для поиска и чтения |
Списочные инструменты Confluence отвечают конвертом { items, nextCursor }: nextCursor — это значение, которое нужно вернуть в аргументе cursor, а не URL и не подписанный токен. У поиска это смещение строк (эндпоинт считает строки), у остальных чтений — курсор самого Confluence, из которого провайдер берёт только сам токен: путь и параметры следующего запроса он собирает сам, поэтому курсор не может увести запрос на чужой адрес. Комментарии с kind: "all" отвечают двумя курсорами (cursors.footer и cursors.inline), потому что продолжать их придётся по отдельности. totalSize у поиска говорит, сколько всего совпадений нашлось, — по нему модель понимает, стоит ли листать дальше.
Дату изменения можно задать абсолютным днём (YYYY-MM-DD) или окном от сегодняшнего дня (-7d, -2w, -1m, -1y): у агента в промпте часов нет, поэтому окно считает провайдер.
Поиск идёт через CQL, но CQL собирает провайдер: модель передаёт типизированные фильтры (текст, пространства, типы, метки, автор, дата), а cql.ts экранирует литералы и подставляет space in (…), currentUser() и ORDER BY. Инструмента с произвольным CQL в surface нет. Текст страницы и комментариев приходит не сырым payload, а markdown-подобной разметкой в поле untrustedContent: adf.ts разбирает Atlassian Document Format в заголовки, списки, таблицы, блоки кода, ссылки, упоминания, панели и статусы, а макросы, медиа и встроенные представления заменяет плейсхолдерами ([Confluence macro: jira], [media: chart]) — провайдер ничего не отрисовывает и никуда не ходит по ссылкам из страницы. Тело режется бюджетом (maxChars в аргументе, потолок — maxBodyChars стенда) и при обрезке отвечает truncated: true и totalChars. Подсветка совпадений в выдержках поиска снимается вместе с разметкой.
Инструменты Weblate
| Tool | Что делает |
|---|
weblate_connection_get | К какому инстансу Weblate и каким аккаунтом подключён токен и личный он или проектный; сам токен не возвращается |
weblate_projects_list | Проекты, видимые аккаунту, постранично |
weblate_project_get | Один проект: слаг, имя, ссылка |
weblate_project_statistics_get | Готовность проекта: строки и слова переведены, требуют правки, не проходят проверки |
weblate_components_list | Компоненты проекта постранично |
weblate_component_get | Один компонент: слаг, имя, система контроля версий, ветка, формат файлов, исходный язык |
weblate_component_statistics_get | Готовность компонента по языкам |
weblate_translations_list | Языки компонента с их состоянием перевода |
weblate_translation_get | Один язык одного компонента целиком: переведено, требует правки, проверки, комментарии, предложения, автор последней правки |
weblate_translation_statistics_get | Статистика одного языка одного компонента |
weblate_units_search | Строки одного языка одного компонента с типизированными фильтрами |
weblate_units_find | Строка по всему, что видит аккаунт, с сужением по проекту, компоненту и языку |
weblate_unit_get | Одна строка целиком: все формы множественного числа исходника и перевода, состояние, контекст, заметка, метки, флаги, проверки |
weblate_unit_comments_list | Комментарии к строке: автор, время, текст |
weblate_unit_suggestions_list | Предложенные переводы строки: автор, голоса, время |
weblate_failing_units_list | Строки, у которых не проходит хотя бы одна проверка, с сужением по проекту, компоненту, языку, тексту и состоянию |
weblate_changes_list | Последние изменения проекта: какая строка, какой язык, что сделано, кем и когда |
weblate_screenshots_list | Скриншоты Weblate и строки, к которым они привязаны |
Списочные инструменты Weblate отвечают конвертом { items, pagination }: pagination.nextPage — номер следующей страницы, а не подписанный курсор. По ссылке next провайдер не ходит сам: Weblate кладёт в тело абсолютный адрес, из которого читается только номер страницы, и только когда он ведёт на настроенный инстанс — ссылка с чужого адреса отбрасывается, а список честно считается законченным. Страница — 20 строк по умолчанию, а perPage сверх потолка стенда (maxPageSize, по умолчанию 100) обрезается, а не отвергается: «дай больше строк» — это запрос, на который стенд отвечает своим потолком, а не ошибка, которую модель выясняет перебором. Состояние между вызовами не хранится, principal и права перепроверяются на каждом вызове.
Модель не получает q: фильтры типизированные (source, target, context, state, failingChecks, suggestions, comments, а у поиска по всему инстансу ещё project, component и language), а строку запроса собирает query.ts — значения всегда в двойных кавычках с экранированием \ и ", поэтому кавычка или обратный слэш внутри запроса остаются значением и не становятся условием. Словарь состояний — собственный словарь Weblate is:: untranslated (по определению самого Weblate — всё, что ниже translated, то есть вместе со строками, помеченными needs-editing), needs-editing, translated, approved, read-only. Поиск без единого фильтра уходит без параметра q, а не с пустым. weblate_failing_units_list — тот же /units/, что и weblate_units_find, с жёсткой клаузой has:check; Weblate сообщает, что строка не проходит проверку, но не то, какая именно проверка упала, поэтому ответ несёт hasFailingCheck, а не список имён проверок.
Строки нормализованы: числовые состояния Weblate (0/10/20/30/100) отображаются в untranslated/needs-editing/translated/approved/read-only, формы множественного числа остаются массивом (перевод, молча короче настоящего, был бы прочитан как правда), текст режется — 200 символов в списке, maxChars у отдельной строки, потолок — maxTextChars стенда — и помечается textTruncated, метки приходят именами, а проект, компонент и язык читаются из вложенной ссылки translation, и только когда она ведёт на настроенный инстанс. Каждый ответ с текстом, написанным на стороне Weblate, помечен untrustedExternalContent: true: это данные, а не инструкция. Скриншоты — только метаданные (имя, файл в репозитории, язык, id строк, адрес картинки на настроенном инстансе): байты провайдер не скачивает. История изменений у Weblate есть у проекта, компонента и перевода, но не у строки, поэтому инструмент проектный, а строка в каждой записи ищется по её id.
weblate_connection_get называет инстанс, аккаунт и вид токена: у Weblate нет эндпоинта «кто я», и подключённый аккаунт определяется по GET /api/users/, который непривилегированный токен видит одной своей строкой. Токен, который умеет перечислять пользователей, отвечает полным списком — тогда аккаунт честно не определяется (пустой внешний id, в карточке адрес инстанса и вид токена), а не угадывается по первой строке.
Возможности и скопы Confluence
Подключение — Atlassian API token вместе с почтой аккаунта Atlassian (Basic-пара). Возможность появляется у пользователя, если её включил оператор (confluence.*Read).
| Возможность | Что открывает |
|---|
identity.read | Сайт Confluence и подключённый аккаунт Atlassian |
spaces.read | Список пространств и карточка пространства |
search.read | Поиск страниц по тексту, пространствам, меткам, автору и дате |
content.read | Страница целиком с текстом, метками и версией |
comments.read | Комментарии страницы: подвал, inline и ответы |
attachments.read | Метаданные вложений страницы |
versions.read | История версий страницы |
Confluence, как и TeamCity, не сообщает через API, какие права выданы конкретному токену: у OAuth-приложений для этого есть /oauth/token/accessible-resources, а у API token аналога нет. Поэтому сужение идёт только по деплой-флагам оператора, а права аккаунта применяет сам Confluence — отказ приходит как ProviderPermissionDenied, и провайдер никогда не пробует другой credential.
Оператор дополнительно может сузить пространства: confluence.allowedSpaces — это список ключей, и он применяется одинаково к поиску, к списку пространств и к прямому чтению страницы, комментариев, вложений и версий. Страница в пространстве вне списка отвечает отказом OperationDeniedByPolicy, а не тихо исчезает из выдачи; в поиск добавляется space in (…), и строки, пространство которых по ответу не подтверждается, отбрасываются. Страница не несёт ключ пространства, только spaceId, поэтому каждое чтение страницы делает дополнительный запрос самого пространства — он же и наполняет ответ ключом и именем, и по нему принимается решение политики; чтения комментариев, вложений и версий с включённой политикой стоят ещё одного чтения страницы, чтобы узнать её пространство.
Скопы, которые нужны токену: классические read:confluence-content.all, read:confluence-space.summary, search:confluence, read:confluence-user (или гранулярные read:page:confluence, read:comment:confluence, read:attachment:confluence, read:space:confluence, read:content.metadata:confluence, read:user:confluence). Классический токен работает по адресу сайта (https://company.atlassian.net); токен со скоупами Atlassian принимает только через шлюз https://api.atlassian.com/ex/confluence/{cloudId} — такой адрес оператор может задать как instances[].baseUrl, и провайдер будет ходить по тому же относительному пути /wiki/.... Сам cloudId провайдер не ищет. Для Server / Data Center скоупов и почты нет: там нужен личный токен доступа, а адрес инстанса задаётся вместе с его контекстным путём (https://wiki.example.corp/confluence).
Запись в surface отсутствует: создание и правка страниц, комментарии, резолв inline-комментариев, вложения, перемещение и администрирование пространств ждут confirmation-фреймворка. Произвольного REST (confluence_rest_call), произвольного CQL и скачивания вложений тоже нет — только метаданные и ссылки, которые отдал сам Confluence.
Сервисный токен Confluence
Подключение может работать от сервисного токена развёртывания: профиль называет сайт, секрет — та же Basic-пара email:token, а граница ресурсов — spaces (ключи пространств). Чувствительных чтений в каталоге нет: поиск, пространства, страницы, комментарии, метаданные вложений и версии остаются сервисно-безопасными, но каталог не умеет отдавать байты вложений, и такая операция, когда появится, обязана прийти с классификацией не ниже чувствительной. Страница, названная по id, сначала раскрывается в своё пространство, поиск сужается до space in (…) по границе ещё на входе, а строки, пространство которых по ответу не подтверждается, отбрасываются вторым замком. Граница сервиса и операторский confluence.allowedSpaces — два независимых сужения: сервисный вызов живёт в их пересечении.
Возможности и скопы Weblate
Подключение — API-токен Weblate вместе с выбором инстанса: адреса задаёт только оператор (weblate.instances), пользователь выбирает инстанс из списка и вставляет токен. Возможность появляется у пользователя, если её включил оператор (weblate.*Read).
| Возможность | Что открывает |
|---|
identity.read | Инстанс Weblate, подключённый аккаунт и вид токена |
projects.read | Список проектов и карточка проекта |
components.read | Компоненты проекта и карточка компонента |
translations.read | Языки компонента и один язык целиком |
units.read | Поиск строк по тексту, контексту и состоянию, чтение одной строки |
checks.read | Строки, у которых не проходит хотя бы одна проверка |
comments.read | Комментарии к строке |
suggestions.read | Предложенные переводы строки |
changes.read | История изменений проекта |
statistics.read | Готовность проекта, компонента и языка |
screenshots.read | Метаданные скриншотов |
Weblate, как и TeamCity с Confluence, не сообщает через API, какие права выданы конкретному токену: токен действует от имени своего пользователя или своего проекта, а «проверить» чтением чужой области провайдер не имеет права. Поэтому сужение идёт только по деплой-флагам оператора (weblate.*Read), а права применяет сам Weblate — отказ приходит как ProviderPermissionDenied, и провайдер никогда не пробует другой credential. Каждая возможность при этом достижима хотя бы одной операцией, поэтому карточка не может показать переключатель, который ничего не делает.
Префиксы токенов Weblate (wlu_ — личный, wlp_ — проектный) доходят до пользователя подписью в карточке (· личный токен, · токен проекта, · токен) и никогда не являются проверкой прав: токен с незнакомым префиксом принимается наравне с остальными, а его права определяет сам Weblate. Проектный токен карточка рекомендует как меньший радиус поражения — это подсказка, а не запрет.
Оператор стенда дополнительно решает, какие из инструментов видит модель: имена weblate_* нужно добавить в tool allow-list пресета QA. Инструменты не принимают ни пользователя, ни credential, ни инстанс, поэтому допуск к ним — решение оператора, а не модели.
Запись в surface отсутствует: предложения перевода, комментарии, правка и утверждение перевода, файлы перевода, autotranslate и операции с репозиторием ждут confirmation-фреймворка. Инструмента с произвольным q и произвольным REST тоже нет: поиск принимает только типизированные фильтры.
Возможности и скопы
Одна возможность = один scope Bitrix24. Возможность появляется у пользователя только если её включил оператор (bitrix24.*Read, у записи — bitrix24.crmCommentWrite) и подключённый вебхук реально получил соответствующий scope — плагин спрашивает это у портала методом scope при подключении и при нажатии «Проверить».
| Возможность | Scope вебхука | Что открывает |
|---|
crm.read | crm | CRM, дела, таймлайн, товарные строки, дубли |
crm.comment.write | crm | Единственная операция записи: комментарий в таймлайн лида, сделки, контакта или компании |
chat.read | im | Чаты, сообщения, поиск по переписке |
openlines.read | imopenlines | Диалоги открытых линий и их история |
user.read | user_brief / user_basic / user | Свой профиль и поиск сотрудников |
department.read | department | Структура компании |
tasks.read | task | Задачи |
calendar.read | calendar | Календарь и занятость |
disk.read | disk | Файлы Диска |
У Bitrix24 нет read-only scope вебхука: crm покрывает и чтение, и запись, поэтому гранулярность «запись отдельно» даёт только конфиг-флаг. crm.comment.write выключен по умолчанию; даже после включения флага операция стартует с политикой deny, пока пользователь (или оператор) явно её не разрешит, — проба scope сама по себе запись не открывает. Остальная запись (*.add, *.update, *.delete), бизнес-процессы, управление пользователями и generic REST/MCP в surface отсутствуют.
Выданные в Bitrix24 права не отменяются: портал всё равно применяет права того пользователя, чей вебхук используется. Возможность, обнаруженная после выдачи нового scope, появляется в карточке выключенной — пользователь включает её сам.
Сервисный токен Bitrix24
Подключение может работать от сервисного вебхука развёртывания: профиль managedServiceCredentials называет портал (хост, в любом регистре — он сравнивается с хостом, который брокер выводит из URL вебхука), секретом служит полный URL сервисного входящего вебхука, а граница ресурсов — единственный вид portals: сервисный вызов обязан отвечать на портале из списка, других идентификаторов у границы нет. Проба (validateServiceCredential) читает только profile и scope; подтвердить read-only она не может — scope crm и читает, и пишет одним битом, — поэтому вердикт всегда «здоров» с предупреждением, а потолок режима обеспечивают классификация операций и граница. Сервисному токену недоступны по классификации: запись в таймлайн, справочник сотрудников (телефоны и почта), дубликаты по телефону и почте, расшифровки звонков, все чтения чатов и открытых линий, календарь занятости и Диск — это чтения, ответы которых принадлежат конкретным людям, а не порталу; CRM-записи, задачи и структура отделов остаются сервисно-безопасными.
Возможности и скопы GitLab
Подключение — personal access token. Возможность появляется у пользователя только если её включил оператор (gitlab.*Read) и токен при подключении реально предъявил соответствующий scope: плагин спрашивает scopes у самого токена (GET /api/v4/personal_access_tokens/self) при сохранении и при нажатии «Проверить». Токен, который не может прочитать себя (старый GitLab, group/project access token), только снижает точность — срез остаётся на деплой-флагах оператора.
| Возможность | Scope токена | Что открывает |
|---|
identity.read | read_user / read_api / api | Свой профиль: логин, имя, инстанс |
projects.read | read_api / api | Проекты, видимость, ветка по умолчанию |
repository.read | read_repository / read_api / api | Дерево, файлы, коммиты, сравнение веток |
search.read | read_api / api | Поиск по проектам, задачам, MR, коммитам, коду |
issues.read | read_api / api | Задачи и комментарии к ним |
merge_requests.read | read_api / api | MR, диффы, обсуждения, одобрения, пайплайны MR |
ci.metadata.read | read_api / api | Пайплайны, джобы, их статусы и метаданные |
ci.logs.read | read_api / api | Содержимое лога джоба |
CI разделён надвое намеренно: лог джоба — это другая утечка, чем список джоб, а раньше обе половины закрывала одна возможность ci.read. Оператор стенда задаёт половины отдельно (ciMetadataRead и ciLogsRead), а старый флаг ciRead продолжает работать и теперь управляет обеими; сохранённая политика ci.read при первом запуске переносится на оба новых id, поэтому выключенный CI не включается сам. Под сервисным токеном доступна только первая половина: ci.metadata.read отвечает, какие джобы были и чем кончились, а ci.logs.read показывает, что они напечатали, и остаётся личным чтением. Любая операция, читающая проект, в сервисном режиме обязана назвать проект или группу внутри границы профиля, а листинг без проекта (gitlab_issues_list, gitlab_merge_requests_list) отказывается — вместо ответа всей картиной, доступной общему аккаунту.
У GitLab нет отдельного read-scope на каждую область, поэтому read_api покрывает почти всё, а read_repository и read_user дают узкие наборы. Права токена не отменяются: GitLab всё равно применяет права своего пользователя. Возможность, появившаяся после выдачи нового scope, приходит в карточку выключенной. Запись (POST/PUT/DELETE), GraphQL, произвольный REST, admin-API, управление токенами, участниками и CI-переменными в surface отсутствуют; merge и запись в репозиторий — тоже. Сервисный токен не превращает эти запреты в разрешения: он только сужает — и дополнительно прячет от общего аккаунта лог джоба.
Оператор стенда дополнительно решает, какие из инструментов видит модель: имена нужно добавить в tool allow-list пресета QA. Инструменты не принимают ни пользователя, ни credential, поэтому допуск к ним — решение оператора, а не модели.
Возможности и скопы TeamCity
Подключение — персональный access token TeamCity. Адрес сервера задаёт оператор в конфигурации развёртывания (teamcity.serverUrl) — он один на всех, поэтому форма подключения спрашивает только токен, а карточка показывает адрес как справочную строку. Если адрес не задан, карточка говорит об этом вместо формы, которая всё равно не сохранится. Возможность появляется у пользователя, если её включил оператор (teamcity.*Read).
| Возможность | Что открывает |
|---|
identity.read | Сервер TeamCity, его версия и подключённый пользователь |
projects.read | Проекты |
buildConfigs.read | Конфигурации сборки |
builds.read | Сборки, карточка сборки, изменения в VCS |
failures.read | Упавшие тесты и проблемы сборки |
logs.read | Фрагмент лога сборки |
queue.read | Очередь сборки |
investigations.read | Расследования падений |
agents.read | Агенты сборки |
artifacts.read | Список артефактов и текст небольших текстовых файлов |
Лог сборки и текст артефакта остаются личными под сервисным токеном: это текст, который напечатала сборка, и общий read-only аккаунт его не читает. Список артефактов — обычное чтение метаданных, но возможность у него одна с текстом, поэтому в сервисном режиме artifacts.read целиком помечена «Требуется личный аккаунт», как и logs.read. В сервисном режиме листинг обязан назвать проект или конфигурацию: teamcity_builds, teamcity_build_configs, teamcity_queue и teamcity_investigations без projectId или buildTypeId отказываются, потому что иначе ответили бы всей картиной сервера, а teamcity_projects собирается из allowlist профиля. Сборка, названная по id, сначала разрешается в свой проект, поэтому прямой teamcity_build проходит ту же проверку, что и листинг. Причины падений (teamcity_build_failures) остаются сервисными: инструмент отвечает упавшими тестами и проблемами сборки, а не логом.
TeamCity, в отличие от GitLab и Bitrix24, не сообщает через API, какие именно права выданы конкретному токену: админ-скоупы невидимы снаружи, а «проверить» записью провайдер не имеет права. Поэтому сужение идёт только по деплой-флагам оператора, а права токена применяет сам TeamCity — отказ приходит как ProviderPermissionDenied, и провайдер никогда не пробует другой credential. Рекомендация пользователю в карточке прямая: создать токен с «Limit per project» и только теми правами чтения, которые нужны.
Запись в surface отсутствует: запуск, перезапуск, отмена сборки, комментарии и теги ждут confirmation-фреймворка; редактирование конфигураций, управление агентами, расследованиями и mute-ами — тоже. Параметры сборки (/parameters, resulting-properties) провайдер не запрашивает вообще, поэтому секретных значений в ответах нет по построению.
Возможности и скопы Jira
Подключение зависит от того, где живёт Jira, и это объявляет оператор в конфигурации развёртывания (jira.sites[].deploymentType). Atlassian Cloud аутентифицирует API-токен Atlassian вместе с e-mail аккаунта через HTTP Basic (email:token); Jira Server / Data Center — личный токен доступа (Personal Access Token) через Bearer, без почты. Провайдер ходит по API того продукта, который объявлен: /rest/api/3 у Cloud, /rest/api/2 у Server / Data Center. Сайт, который отвечает serverInfo.deploymentType другого продукта, отклоняется на подключении с подсказкой, какое значение поставить.
Список сайтов задаёт только оператор — пользователь выбирает сайт из списка и вставляет токен (и почту, если сайт облачный); произвольный хост ввести нельзя. Возможность появляется у пользователя, если её включил оператор (jira.*Read).
| Возможность | Что открывает |
|---|
identity.read | Подключённый пользователь Atlassian и сайт, на котором он работает |
issues.read | Поиск задач со всем словарём фильтров (включая фильтр по истории поля — WAS/CHANGED — и разрешение имени в идентификатор пользователя) и карточка задачи с описанием, связями и кастомными полями |
comments.read | Комментарии задачи, включая ограниченные (с пометкой видимости) |
attachments.read | Метаданные вложений задачи: имя, тип, размер, автор |
transitions.read | Доступные переходы статуса и их обязательные поля |
projects.read | Карточка проекта |
fields.read | Схема полей сайта, включая кастомные, — она же источник id для фильтра по кастомному полю |
Jira, как и TeamCity, не сообщает через API, какие права выданы конкретному API-токену: токен действует от имени пользователя и наследует его права целиком, а «проверить» записью провайдер не имеет права. Поэтому сужение идёт только по деплой-флагам оператора, а права аккаунта применяет сама Jira — отказ приходит как ProviderPermissionDenied (задача, проект или поле закрыты схемой прав, issue security level или схемой ролей), и провайдер никогда не пробует другой credential. Единственная проверка, которую провайдер делает сам при подключении, — тип развёртывания: сайт, который отвечает deploymentType не тем продуктом, который объявил оператор, отклоняется с подсказкой, какое значение поставить, потому что Cloud и Server / Data Center — разные API, и молча считать их совместимыми спецификация запрещает.
Запись в surface отсутствует: создание, правка, назначение, комментарии и переходы ждут pending actions и карточки подтверждения; удаление, администрирование проектов и workflow, произвольный REST и скачивание содержимого вложений — тоже. Провайдер читает только объявленный allow-list эндпоинтов /rest/api/3 (гейт пакета падает на любом пути вне него), а легаси-эндпоинт /rest/api/3/search, удалённый Atlassian, не используется: поиск идёт через /rest/api/3/search/jql.
Сервисный токен Jira
Подключение может работать от сервисного токена развёртывания: профиль называет сайт, секрет — обычный API-токен Atlassian в Basic-паре, а граница ресурсов — projects (ключи проектов, сравнение без учёта регистра; числовой id засчитывается, если Jira отдала его в выдаче). Сервисному токену недоступны чтения вложений — даже список метаданий помечен чувствительным, потому что следующий естественный шаг агента — скачать файл. Фильтры JQL по имени человека в сервисном режиме отказывают целиком — это чтение корпоративного справочника, у которого нет проекта в границе, — а фильтры по accountId и me остаются. Задача, названная по ключу, сначала раскрывается в свой проект, и чтение с уровнем безопасности, который сервисный токен не проходит, отвечает ResourceNotFound, как для чужой задачи.
Адрес TeamCity задаёт оператор: сервер один на весь стенд, поэтому teamcity.serverUrl — обычная настройка развёртывания, а не поле формы. Пользователь вводит только токен, и токен тратится ровно по этому адресу. Рядом оператор задаёт политику адресов: какие адреса этот стенд готов набирать вообще (политика осталась от времён, когда адрес вводил пользователь, и закрывает переезд конфига на чужой хост).
teamcity:
enabled: true
serverUrl: https://teamcity.example.internal
network:
mode: allowlist # allowlist | trusted-private
allowedHosts:
- teamcity.example.internal
- "*.corp.example"
allowedCidrs:
- 10.20.0.0/16 # только для адресов, записанных цифрами
allowedPorts:
- 443
- 8111
allowHttp: false
Правила простые и проверяются дважды — при сохранении подключения и на каждом вызове инструмента, поэтому ужесточение политики закрывает и ранее сохранённые подключения:
- только
http/https, HTTPS обязателен вне явного allowHttp; в URL не может быть credentials, query и fragment;
- имя хоста должно попасть в
allowedHosts (точное имя или *.суффикс), а адрес, записанный цифрами, — в allowedCidrs;
- порт должен быть в
allowedPorts: по умолчанию разрешён только порт схемы, поэтому TeamCity на своём стандартном :8111 требует явного allowedPorts: [8111];
- редиректы не отслеживаются (
redirect: "error"), токен уходит только в заголовке Authorization: Bearer, в URL и теле его нет;
mode: trusted-private — для доверенной корпоративной сети: имена хостов разрешены любые, а закрытые диапазоны (10/8, 172.16/12, 192.168/16, 127/8) подставляются как allowedCidrs по умолчанию. Он предполагает, что стенд доверяет своему DNS; если это не так, нужен allowlist.
Пустая политика (allowedHosts и allowedCidrs пусты) — это не ошибка загрузки, а «ничего не подключить»: плагин пишет об этом предупреждение в лог при старте, а карточка отвечает пользователю отказом. Опечатки в самой политике — наоборот, падение на старте: молча выброшенный шаблон хоста оставил бы стенд без объяснения.
Если для этого сервера заведён сервисный токен, форма вместо поля с токеном показывает чекбокс «Использовать сервисный токен»: при defaultForNewConnections: true он отмечен, кнопка называется «Подключить сервисный токен», а адрес сервера остаётся той же справочной строкой — его по-прежнему задаёт оператор. Подключённая карточка называет псевдоним сервисного профиля вместо аккаунта, кнопками «Использовать личный аккаунт» и «Использовать сервисный токен» переключает режимы, а «Изменить доступ» сужает список проектов, с которыми работает подключение.
Конфигурация TeamCity
teamcity:
enabled: true
serverUrl: https://teamcity.example.internal # один адрес на весь стенд
network:
mode: allowlist
allowedHosts: [teamcity.example.internal]
allowedPorts: [443]
allowHttp: false
identityRead: true
projectsRead: true
buildConfigsRead: true
buildsRead: true
failuresRead: true
logsRead: true
queueRead: true
investigationsRead: true
agentsRead: true
artifactsRead: true
maxLogLines: 1000 # потолок строк в ответе по логу
maxLogBytes: 262144 # сколько лога вообще скачиваем
defaultArtifactBytes: 262144 # бюджет чтения артефакта по умолчанию
maxArtifactBytes: 1048576 # жёсткий потолок чтения артефакта
streamTimeoutMs: 30000 # таймаут для логов и артефактов
retries: 2 # повторы для 429/5xx и сетевых сбоев, включая таймаут: вызов стоит до retries × timeoutMs
Токен создаётся в TeamCity (Profile → Access Tokens), при желании с «Limit per project» и read-правами, вводится один раз в карточке и шифруется тем же конвертным хранилищем, что и остальные секреты.
Чего в TeamCity-провайдере ещё нет
Спецификация docs/specs/providers-teamcity.md описывает целевую архитектуру; в этой версии реализована read-only фаза, как и у Bitrix24 с GitLab:
- Запись (§12.2): запуск, перезапуск, отмена сборки, комментарий и теги — вместе с ними отложены pending actions и карточка подтверждения (§24). Пока в каталоге нет ни одной операции, которая может изменить TeamCity.
- Автоматический постраничный обход (§18): сейчас один вызов — один ограниченный запрос, а
pagination.hasMore сообщает модели, что страница обрезана. Пятистраничная агрегация потребовала бы ходить по nextHref, чего провайдер не делает.
- Кэш (§20) отсутствует полностью, поэтому и межпользовательской утечки через кэш быть не может.
- Rate limiting (§19): в личном режиме лимиты запросов в минуту и на сервер не считаются; вместо них — ограниченные размеры страниц и повторы с backoff для 429/5xx.
- Свои CA и
tlsVerify: false (§8): самоподписанный сертификат TeamCity заканчивается понятным TlsFailure, а не тихой проверкой без доверия. Доверие к сертификату задаётся на уровне стенда.
- Webhooks (§21), запись расследований и mute-ов, одобрение сборок, admin-операции и плановые задания (§25–26) — не подключены.
- Секретные параметры сборки (§15) в surface не попадают, потому что провайдер их не читает; отдельной «показать скрытое» нет и не планируется.
Как подключается Confluence
Сайты Confluence задаёт только оператор: пользователь выбирает из списка, вводит почту своего аккаунта Atlassian и вставляет API token. Произвольный хост ввести нельзя, и адрес не сохраняется в credential: он перечитывается из конфига на каждом вызове, поэтому удаление или переезд сайта немедленно закрывает доступ к сохранённому токену (CredentialRevoked), и подмены на другой сайт не происходит.
Почта не секрет, но она — вторая половина Basic-пары, поэтому лежит в том же зашифрованном credential, что и токен ({ instanceId, email, token }): пара «токен + аккаунт» не может быть собрана из разных записей. Redirect не отслеживается (redirect: "error"), секрет уходит только в заголовке Authorization: Basic …, в URL и теле его нет. Если сайтов нет вообще, карточка честно сообщает, что подключать нечего; если сайт один, выбор не показывается.
Конфигурация Confluence
confluence:
enabled: true
allowInsecureHttp: false # только для dev-стенда
instances:
- id: company
label: Company
baseUrl: https://company.atlassian.net
deploymentType: cloud # cloud (по умолчанию) | server | data-center
# токен со скоупами требует шлюза Atlassian:
# - id: company-scoped
# label: Company (scoped token)
# baseUrl: https://api.atlassian.com/ex/confluence/<cloudId>
# самохостящаяся Confluence Server / Data Center (контекст — в baseUrl):
# - id: corp
# label: Corporate Wiki
# baseUrl: https://wiki.example.corp/confluence
# deploymentType: server # личный токен доступа, /rest/api
identityRead: true
spacesRead: true
searchRead: true
contentRead: true
commentsRead: true
attachmentsRead: true
versionsRead: true
allowedSpaces: [] # ключи пространств; пусто = все, что видит аккаунт
defaultBodyChars: 20000 # бюджет тела страницы по умолчанию
maxBodyChars: 60000 # потолок бюджета, который может попросить модель
maxResults: 50 # строк в ответе любого списка (потолок для limit)
maxReplyParents: 10 # сколько комментариев могут принести ответы
retries: 2 # повторы для 429/5xx и сетевых сбоев, включая таймаут: вызов стоит до retries × timeoutMs
API token выпускается в профиле Atlassian (Manage profile → Security → API tokens), вводится один раз в карточке и шифруется тем же конвертным хранилищем, что и остальные секреты. Если стенду нужен OAuth-аккаунт Atlassian общего вида, а не персональный токен, — это отдельная фаза.
Чего в Confluence-провайдере ещё нет
Спецификация docs/specs/providers-confluence.md описывает целевую архитектуру; в этой версии реализована read-only фаза, как и у остальных провайдеров:
- OAuth 2.0 3LO (раздел «Authentication»): подключение идёт API token плюс почта аккаунта, поэтому нет ни общего OAuth-приложения, ни refresh-цикла, ни общего слоя
providers/atlassian с accessible-resources. Выбор ресурса сводится к выбору сайта из конфига оператора.
- Запись (раздел «Write operations»): создание и правка страниц, комментарии, резолв inline-комментариев — вместе с ними отложены pending actions, карточка подтверждения и оптимистичная блокировка по версии страницы. Пока в каталоге нет ни одной операции, которая может изменить Confluence.
- Скачивание вложений (раздел «Attachments»): сейчас возвращаются только метаданные и ссылки, которые отдал Confluence; ни байты, ни временное хранилище на пользователя не реализованы.
- Произвольный CQL и произвольный REST (разделы «CQL policy», «Tool surface»): поиск принимает только типизированные фильтры, инструментов
confluence_search_cql и confluence_rest_call нет и не планируется в этой фазе.
- Хлебные крошки (раздел «MVP feature set»): эндпоинт предков страницы отдаёт только id и тип, а названия потребовали бы отдельного чтения каждой страницы, поэтому в ответе есть только
parentId.
- Чтение блогов (раздел «Optional MVP+»): поиск умеет возвращать
blogpost, но открыть его нечем — отдельного чтения блогов в surface нет.
- Продолжение большого тела (раздел «Response size controls»): тело режется бюджетом и помечается
truncated, но сегментного чтения (confluence_get_page_segment) нет; страница, чей JSON на стороне Confluence больше maxResponseBytes, отвечает ResultTooLarge.
- Кэш (раздел «Caching») отсутствует полностью, поэтому и межпользовательской утечки через кэш быть не может; пагинация stateless, подписанных continuation-токенов нет.
- Rate limiting (раздел «Rate limiting»): в личном режиме лимиты по principal, ресурсу и глобальные не считаются; вместо них — ограниченные размеры ответов и повторы с backoff для 429/5xx.
- Семантический индекс (раздел «Optional semantic index») и webhooks (раздел «Compatibility») — не подключены.
Confluence Server / Data Center подключён: объявите инстанс с deploymentType: server, и чтения пойдут по v1 API (/rest/api) с личным токеном доступа, а тела страниц — в storage-разметке.
Сервисный токен Weblate
Подключение может работать от сервисного токена развёртывания: профиль называет инстанс, секрет — обычный API-токен Weblate (префикс значения роли не играет), а граница ресурсов — projects (слаги проектов; компонент принадлежит проекту, поэтому чтение компонента сверяет его проект). Чувствительных чтений в каталоге нет: всё, что читает провайдер, — переводческие метаданные, а changes.list — собственный журнал изменений Weblate, а не сырой вывод VCS. Чтение, названное по id строки, сначала раскрывается в свой проект; скриншоты вне границы и с непроверяемым проектом из выдачи выпадают, а листинг проектов собирается из allowlist с unavailableResources для того, что токен не видит.
Инстансы Weblate задаёт только оператор: пользователь выбирает инстанс из списка и вставляет API-токен, произвольный хост ввести нельзя. Если инстанс один, выбор не показывается; если инстансов нет, карточка честно сообщает, что подключать нечего, и формы не показывает.
Правила адреса инстанса — те же, что у GitLab (то, что защищает брокер от SSRF): только http/https, HTTPS обязателен вне явного allowInsecureHttp для стенда; без credentials и query в URL; путь допустим (Weblate за обратным прокси в подкаталоге), хвостовой слэш срезается; id инстанса — стабильный ключ, который и хранит секрет. Адрес не сохраняется в credential: он перечитывается из конфига на каждом вызове, поэтому удаление или переезд инстанса немедленно закрывает доступ к сохранённому токену (CredentialRevoked), и подмены на другой инстанс не происходит. Redirect не отслеживается (redirect: "error"), а токен уходит только в заголовке Authorization: Token … — в URL и теле его нет.
Токен выпускается в профиле Weblate (настройки профиля → API tokens), вводится один раз в карточке, шифруется тем же конвертным хранилищем, что и остальные секреты, и после сохранения не показывается и не копируется. Рядом карточка подсказывает, что для минимума прав лучше подходит токен проекта (wlp_…), ограниченный одним проектом.
Конфигурация Weblate
weblate:
enabled: true
allowInsecureHttp: false # только для dev-стенда
instances:
- id: corp
label: Corporate Weblate
baseUrl: https://weblate.example.com
# Weblate в подкаталоге сохраняет путь:
# - id: corp-weblate
# label: Corp Weblate
# baseUrl: https://example.com/weblate
identityRead: true
projectsRead: true
componentsRead: true
translationsRead: true
unitsRead: true
checksRead: true
commentsRead: true
suggestionsRead: true
changesRead: true
statisticsRead: true
screenshotsRead: true
maxTextChars: 4000 # символов одной строки, которую видит модель
maxPageSize: 100 # потолок страницы; по умолчанию провайдер просит 20
retries: 2 # повторы для 429/5xx и сетевых сбоев, кроме таймаута: он завершает вызов
Список инстансов принимает не больше 16 записей; id — строчные латинские буквы, цифры и дефисы (до 32 символов), без повторов; подпись по умолчанию — имя хоста. Опечатка в инстансе роняет загрузку конфига, а не выбрасывает инстанс молча: пользователь иначе получил бы провайдер, к которому нельзя подключиться, и никакого объяснения. Пустой список — это состояние, а не падение: провайдер зарегистрирован и бездействует, а отказывает форма подключения.
Чего в Weblate-провайдере ещё нет
Спецификация docs/specs/providers-weblate.md описывает целевую архитектуру; в этой версии реализована read-only фаза, как и у остальных провайдеров:
- Запись (раздел «Write operations»): предложения перевода, комментарии, правка и утверждение перевода, файлы перевода, autotranslate и операции с репозиторием — вместе с ними отложены pending actions и карточка подтверждения. Пока в каталоге нет ни одной операции, которая может изменить Weblate.
- Скачивание скриншотов (раздел «Screenshots and binary data»): сейчас только метаданные, id связанных строк и адрес картинки на настроенном инстансе; ни байты, ни лимит размера, ни проверка MIME, ни временное хранилище на пользователя не реализованы.
- История изменений по строке (раздел «Change history»): Weblate отдаёт историю по проекту, компоненту и переводу, но не по строке, поэтому инструмент проектный, а нужная строка ищется по id в каждой записи; отдельного чтения истории одной строки нет и появиться не может, пока такого эндпоинта нет у самого Weblate.
- Произвольный
q (раздел «Search and filters»): поиск принимает только типизированные фильтры. Грамматика объявляет четыре текстовых поля строки (source, target, context, note), но note ни один инструмент не передаёт, так что для модели он недостижим.
- Определение возможностей и код
CAPABILITY_UNAVAILABLE (разделы «Permissions», «Compatibility»): Weblate не сообщает, что разрешено конкретному токену, а выводить права из одного удачного чтения провайдер отказывается, поэтому никакого discovery нет; чтение, которого нет в этой версии Weblate, отвечает 405 и приходит как ResourceNotFound с пояснением, что операцию не поддерживает сама версия.
- Кэш (раздел «Caching») отсутствует полностью, поэтому и межпользовательской утечки через кэш быть не может; пагинация stateless, подписанных continuation-токенов нет.
- Rate limiting (раздел «Rate limiting»): в личном режиме лимитов на аккаунт и на инстанс нет; вместо них ограниченные страницы и текст, повторы с backoff (включая
Retry-After, если он числовой) и полный отказ от повторов на 4xx.
- Продолжение большого ответа (раздел «Response size controls»): тело больше
maxResponseBytes отвечает ResultTooLarge, а сегментного чтения нет.
- SSRF-политика по CIDR и DNS (раздел «API base»): правило провайдера — список инстансов оператора и HTTPS по умолчанию, потому что Weblate обычно self-hosted; «адрес объявлен оператором» заменяет «адрес публичный».
- Аудит и OTel-инструменты (раздел «Observability»): провайдер не публикует ни метрик, ни спанов, а слой интеграций пишет в audit только provider, operation, result и source session id — текст локализации в журнал не попадает.
Отложенный объём — построчно, с тем, что код делает сегодня, с уже записанным дефолтом и с тем, что действительно мешает, — сведён в docs/specs/providers-deferred.md; там же разобран GitLab и там же лежат вопросы, которые рабочая полоса решать не вправе (OAuth против PAT, девять вопросов §37).
Как подключается GitLab
Инстансы GitLab задаёт только оператор: пользователь выбирает из списка и вставляет токен, произвольный хост ввести нельзя. Если инстанс один, выбор не показывается; если инстансов нет, карточка честно сообщает, что подключать нечего.
Правила адреса инстанса (то, что защищает брокер от SSRF): только http/https, HTTPS обязателен вне явного allowInsecureHttp для стенда; без credentials, query и fragment в URL; путь допустим (GitLab, смонтированный в подкаталог), хвостовой слэш срезается; id инстанса — стабильный ключ, который и хранит секрет. Адрес не сохраняется в credential: он перечитывается из конфига на каждом вызове, поэтому удаление или переезд инстанса немедленно закрывает доступ к сохранённому токену (CredentialRevoked), и подмены на другой инстанс не происходит. Redirect не отслеживается (redirect: "error"), токен уходит только в заголовке PRIVATE-TOKEN, в URL и теле его нет.
Если для выбранного инстанса развёртывание завело сервисный токен, форма подключения получает чекбокс «Использовать сервисный токен». При defaultForNewConnections: true он уже отмечен, и поле токена не показывается: подключается общий read-only аккаунт, а кнопка называется «Подключить сервисный токен»; снятая галочка возвращает форму с personal access token. Подключённая карточка говорит, на каком аккаунте работает подключение, переключает режимы кнопками «Использовать личный аккаунт» и «Использовать сервисный токен», а сузить границу доступа предлагает кнопкой «Изменить доступ».
Конфигурация GitLab
gitlab:
enabled: true
allowInsecureHttp: false # только для dev-стенда
instances:
- id: gitlab-com
label: GitLab.com
baseUrl: https://gitlab.com
- id: corp
label: Corporate GitLab
baseUrl: https://gitlab.example.internal
identityRead: true
projectsRead: true
repositoryRead: true
searchRead: true
issuesRead: true
mergeRequestsRead: true
ciMetadataRead: true # пайплайны, джобы, их статусы; прежний ciRead задаёт обе половины
ciLogsRead: true # содержимое лога джоба: только личный аккаунт
maxFileBytes: 131072 # сколько тела файла отдаём модели
maxJobLogBytes: 262144 # сколько лога джобы отдаём модели
maxSearchResults: 50 # потолок per page у поиска
retries: 2 # повторы для 429/5xx и сетевых сбоев, кроме таймаута: он завершает вызов
OAuth-приложение для GitLab не нужно: подключение идёт персональным токеном. Токен создаётся в GitLab (User settings → Access tokens) с read-scope, вводится один раз в карточке, шифруется тем же конвертным хранилищем, что и вебхук Bitrix24, и после сохранения не показывается и не копируется.
Чего в GitLab-провайдере ещё нет
Спецификация docs/specs/providers-gitlab.md описывает целевую архитектуру; в этой версии сознательно реализована только read-only фаза, как и у Bitrix24:
- OAuth Authorization Code + PKCE (§8.1) — вместо него PAT (§8.2). Для OAuth нужны callback-маршрут на хосте, одноразовый state и refresh-цикл; это отдельная фаза.
- Пользовательская граница ресурсов (§12): в личном режиме доступ ограничен правами токена и персональной политикой возможностей, но не выбранным набором проектов/групп. Выбранный набор есть только в сервисном режиме: там границу задаёт администратор в профиле, а пользователь может её сузить, но не расширить.
- Pending actions и подтверждения (§15) — вместе с ними отложены все записи: комментарии, создание задач и MR, запуск и отмена пайплайнов.
- Кэш (§21) отсутствует полностью, поэтому и межпользовательской утечки через кэш быть не может; пагинация stateless, подписанных continuation-токенов нет.
- Webhooks (§23) и официальный GitLab MCP (§24) — не подключены; REST API v4 остаётся единственным бэкендом.
Отдельно от этого списка: у GitLab нет ручек размера страницы и тела, которые обещает §18 спеки (perPage ограничен константой LIST_MAX_PER_PAGE, diff — DIFF_BUDGET), нет обещанных §7 и §28 tls.caBundle и network.*, а чек-лист §36 помечает как приёмку read-only MVP то, что лежит в отложенном. Построчная сводка — с тем, что код делает сегодня, с записанным дефолтом и с тем, что мешает, — в docs/specs/providers-deferred.md.
Как подключается Jira
Сайты Jira задаёт только оператор: пользователь выбирает сайт из списка и вводит API-токен с e-mail, произвольный хост ввести нельзя. Если сайт один, выбор не показывается; если сайтов нет, карточка честно сообщает, что подключать нечего.
Правила адреса сайта (то, что защищает брокер от SSRF): только http/https, HTTPS обязателен вне явного allowInsecureHttp для стенда; без credentials, query и fragment в URL; путь допустим (Jira за обратным прокси в подкаталоге), хвостовой слэш срезается; id сайта — стабильный ключ, который и хранит секрет. Адрес не сохраняется в credential: он перечитывается из конфига на каждом вызове, поэтому удаление или переезд сайта немедленно закрывает доступ к сохранённому токену (CredentialRevoked), и подмены на другой сайт не происходит. Redirect не отслеживается (redirect: "error"), а токен уходит только в заголовке Authorization: Basic …, собранном из email:token — в URL и теле его нет.
Конфигурация Jira
jira:
enabled: true
allowInsecureHttp: false # только для dev-стенда
sites:
- id: company
label: company.atlassian.net
baseUrl: https://company.atlassian.net
deploymentType: cloud # cloud (по умолчанию) | server | data-center
- id: sandbox
label: Sandbox
baseUrl: https://sandbox.atlassian.net
# самохостящаяся Jira Server / Data Center:
# - id: corp
# label: Jira Server
# baseUrl: https://jira.example.corp
# deploymentType: server # личный токен доступа, /rest/api/2
fieldAliases: # имена кастомных полей ЭТОГО сайта: алиас → customfield_ id
product: customfield_10010 # id берётся из jira_get_fields; в репозитории их нет
identityRead: true
issuesRead: true
commentsRead: true
attachmentsRead: true
transitionsRead: true
projectsRead: true
fieldsRead: true
defaultSearchLimit: 20 # строк в ответе поиска, если модель не просила
maxSearchLimit: 100 # потолок страницы; больше Jira не отдаёт
maxCommentLimit: 100 # потолок страницы комментариев
maxTextChars: 20000 # бюджет одного описания, комментария или rich-поля
retries: 2 # повторы для 429/5xx и сетевых сбоев, включая таймаут: вызов стоит до retries × timeoutMs
OAuth-приложение для Jira не нужно: подключение идёт токеном. Для Cloud токен создаётся в аккаунте Atlassian (id.atlassian.com → Безопасность → API-токены), вводится один раз в карточке и шифруется тем же конвертным хранилищем, что и остальные секреты — вместе с e-mail аккаунта, потому что Jira Cloud аутентифицирует пару email:token, а не токен отдельно. Для Server / Data Center токен создаётся в профиле самой Jira (Профиль → Личные токены доступа) и хранится один, без почты: он аутентифицируется как Bearer. После сохранения секрет не показывается и не копируется.
fieldAliases — тоже настройка развёртывания, а не знание провайдера: какие кастомные поля инстанса отвечают на «продукт», «команду» или «окружение» — это свойство конкретного сайта, поэтому в пакете нет ни одного такого id, а оператор объявляет свои имена алиасами и видит их в ответе jira_get_fields. Опечатка в алиасе роняет загрузку конфига, как и опечатка в списке сайтов: алиас, который молча никуда не указывает, отвечал бы «задач нет» вместо «нет такого поля». Значения алиасных полей также известны только инстансу — их читают из реальных задач (jira_get_issue с include: ["custom_fields"]), а не угадывают.
Чего в Jira-провайдере ещё нет
Спецификация docs/specs/providers-jira.md описывает целевую архитектуру; в этой версии реализована read-only фаза, как и у Bitrix24, GitLab и TeamCity:
- OAuth 2.0 3LO с ротацией refresh-токенов (§Authentication): вместо него API-токен с Basic-аутентификацией. Для OAuth нужны callback-маршрут на хосте, одноразовый state с PKCE, per-credential lock на ротацию refresh-токена и
reauth_required при invalid_grant — это отдельная фаза. Токен, отклонённый Jira (401), сейчас отвечает CredentialRevoked, а карточка предлагает заменить токен.
- Общий слой Atlassian (§Shared Atlassian account model):
providers/atlassian/ — общий аккаунт и ресурсы для Jira и Confluence, несколько сайтов на один аккаунт. Пока Jira самостоятельный каталог, как и остальные три провайдера, а сайт выбирается при подключении.
- Привязка workspace → проект (§Workspace binding): allowlist проектов на уровне рабочего пространства — дополнительное сужение, которого сейчас нет; доступ ограничен правами аккаунта и персональной политикой возможностей.
- Raw JQL (§JQL policy, advanced mode): модели доступны только типизированные фильтры. Отдельный JQL-инструмент потребовал бы валидации через parse-API Jira, лимитов на ширину запроса и аудита исходной и нормализованной строки. Решение зафиксировано: растёт словарь типизированных фильтров, а не доступа к строке запроса — так появился фильтр по истории, из-за которого raw JQL главным образом и просили. Произвольный запрос остаётся отдельным инструментом со всеми перечисленными границами, и без них он не подключается.
- Jira Server / Data Center: подключён как второй продукт (см.
deploymentType выше) — чтения идут по /rest/api/2 с личным токеном доступа. Не подключено то, чего у v2 API нет вовсе: enhanced search с continuation-токеном (у Server / Data Center пагинация по offset, и это то, что отдаёт провайдер) и Cloud-специфичные поля ответа вроде accountType.
- Запись (§Write operations):
jira_prepare_* вместе с pending actions, payload hash, сроком годности и перепроверкой прав на подтверждении. Пока в каталоге нет ни одной операции, которая может изменить Jira, и ни одного инструмента со словом prepare.
- Скачивание вложений (§Attachments): сейчас только метаданные. Будущий
jira_get_attachment обязан заново проверять видимость задачи, иметь лимит байт и allowlist типов.
- История кастомных полей и остальные операторы истории:
history покрывает WAS/CHANGED над шестью полями, у которых Jira ищет историю, — «побывали в статусе», «двигали приоритет за две недели», «переназначали на вот кого». За границей остались WAS IN/WAS NOT, комбинированная форма и ключевое слово (окно выражается парой /), а история кастомного поля по-прежнему читается только внутри одной задачи (): какое значение держало поле инстанса и что это значит, знает только развёртывание, и фильтр по в истории был бы угадыванием. Чтение кастомных полей идёт селектором , поэтому на сайте с очень большим числом полей такой ответ может упереться в .
Инструменты Test IT
Test IT — система управления тестированием: тест-библиотека, тест-планы, прогоны
и результаты. Все 22 инструмента только читают; каждый называет capability, и
выключенную оператором возможность агент не получает вовсе.
| Tool | Что делает |
|---|
testit_connection_get | К какой инсталляции подключено и видит ли токен проекты |
testit_projects | Проекты с числом тест-кейсов, чек-листов и автотестов |
testit_project_get | Карточка проекта: описание, счётчики, схема атрибутов |
testit_sections | Разделы (папки) тест-библиотеки проекта |
testit_work_items | Тест-кейсы, чек-листы и общие шаги проекта по фильтрам |
testit_work_item_get | Тест-кейс целиком: шаги, предусловия, атрибуты, теги, вложения |
testit_work_item_history | История изменений: кто что поменял, из чего в что |
testit_work_item_comments | Комментарии к тест-кейсу |
testit_work_item_test_results | История результатов тест-кейса по прогонам и конфигурациям |
testit_test_plans | Тест-планы проекта со статусом и сроками |
testit_test_plan_get | Карточка тест-плана |
testit_test_plan_summary | Прогресс плана: тест-поинты, автоматизация, дефекты |
testit_test_runs | Прогоны проекта по состояниям, датам и тест-плану |
testit_test_run_get | Карточка прогона |
testit_test_run_results | Что было в прогоне: тест-поинты с итогом и результатами |
testit_test_result_get | Результат теста: шаги, длительность, сообщение, трейсы |
testit_test_result_attachments | Метаданные вложений результата |
testit_attachment_metadata | Метаданные вложения |
testit_attachment_text | Текст небольшого текстового вложения (бинарные и архивы отбиваются) |
testit_auto_tests | Автотесты проекта: класс, пространство имён, стабильность |
testit_auto_test_get | Карточка автотеста: шаги, setup, teardown, последний результат |
Возможности и скопы Test IT
| Возможность | Что открывает |
|---|
projects.read | Проекты и карточка проекта, проба подключения |
sections.read | Разделы тест-библиотеки |
workItems.read | Тест-кейсы, чек-листы, общие шаги |
history.read | История изменений тест-кейса |
comments.read | Комментарии тест-кейса |
testPlans.read | Тест-планы, карточка и сводка |
testRuns.read | Прогоны и карточка прогона |
testResults.read | Результаты прогона, отдельный результат, история результатов |
autoTests.read | Автотесты |
attachments.read | Метаданные вложений и текст текстовых файлов |
configurations.read | Конфигурации проекта |
Сужения по «выданным правам» здесь нет: API v2 не сообщает, какие права у
токена (в отличие от вебхука Bitrix24 с его scope и GitLab с
personal_access_tokens/self). Поэтому список возможностей — ровно то, что
разрешил оператор стенда, а дальше отказывает сам Test IT: 403 приходит
модели как ProviderPermissionDenied, а не превращается в пустой ответ.
Три читающих списка Test IT помечает устаревшими (GET /projects,
GET /projects/{projectId}/workItems, GET /autoTests). Они оставлены: это
единственные GET-чтения этих коллекций, а поиск (POST /workItems/search),
статистика прогона и глобальный поиск — POST-эндпоинты, которых в каталоге нет
по правилу «каждая операция каталога — GET». Что из-за этого недоступно и чем
заменено — в providers-testit.md.
Сервисный токен Test IT
Подключение может работать от сервисного токена развёртывания: профиль называет инсталляцию, секрет — обычный API-токен, а граница ресурсов — projects (id проектов, точное сравнение — иерархии проектов у Test IT нет). Сервисному токену недоступны вложения целиком: даже метаданные не несут проект, поэтому глобальный id нечем сверить с границей — каталог отвечает отказом, а не молча; тело вложения чувствительно и в личном режиме читается только у маленьких текстовых файлов. Чтение, адресованное id (тест-кейс, план, прогон, результат), сначала раскрывается в свой проект, и чтение вне границы отказывает до обращения к собственно объекту; листинг projects.list собирается из allowlist, а не из того, что видит токен.
Test IT бывает облачный и self-hosted, поэтому адрес задаёт оператор стенда
списком инсталляций (testit.instances) — пользователь выбирает инсталляцию из
списка и вводит только свой API-токен:
testit:
instances:
- id: cloud
label: Test IT Cloud
baseUrl: https://team.example.testit.software
Правила те же, что у GitLab и Jira: только https:// (кроме осознанной
лазейки allowInsecureHttp для внутреннего стенда), никаких credentials в URL,
query и фрагмента, а хвостовой слэш снимается. Опечатка оператора падает при
загрузке конфига. Адрес не хранится в подключении: он перечитывается из
конфига на каждом вызове, поэтому удаление или переадресация инсталляции
закрывает и уже созданные подключения. Если инсталляций нет, карточка говорит
об этом и формы не показывает.
Токен Test IT едет только заголовком Authorization: PrivateToken …. Владельца
токена API v2 не сообщает (эндпоинта вида /me в спецификации нет), поэтому
после проверки карточка показывает инсталляцию и число видимых проектов, а не
имя аккаунта.
Конфигурация Test IT
testit:
enabled: true
allowInsecureHttp: false # лазейка для внутреннего стенда; в проде HTTPS
instances:
- id: cloud
label: Test IT Cloud
baseUrl: https://team.example.testit.software
projectsRead: true # флаги возможностей: выключенная не доходит до агента
sectionsRead: true
workItemsRead: true
historyRead: true
commentsRead: true
testPlansRead: true
testRunsRead: true
testResultsRead: true
autoTestsRead: true
attachmentsRead: true
configurationsRead: true
defaultResults: 20 # размер списка, когда модель не назвала лимит
maxResults: 100 # жёсткий потолок одного ответа-списка
defaultAttachmentBytes: 262144 # бюджет чтения вложения по умолчанию
maxAttachmentBytes: 10485760 # жёсткий потолок чтения вложения (10 MiB)
attachmentTimeoutMs: 30000 # таймаут скачивания вложения
retries: 2 # повторы для 429/5xx и сетевых сбоев, кроме таймаута: он завершает вызов
Чего в Test IT-провайдере ещё нет
Спецификация docs/specs/providers-testit.md описывает целевую архитектуру; в
этой версии реализована read-only фаза, как и у остальных пяти провайдеров:
- Поиск и статистика через POST:
POST /workItems/search,
POST /testRuns/{id}/statistics/filter, POST /testResults/statistics/filter
и глобальный поиск не используются — в каталоге нет ни одной не-GET операции.
Фильтр по имени применяется к возвращённой странице, статистика прогона
заменена чтением тест-поинтов прогона и сводкой тест-плана.
- Скачивание вложений: доступен только текст небольших текстовых файлов;
архивы, изображения и документы отбиваются по имени из метаданных, а не
инлайнятся.
- Запись (комментарии, правка тест-кейсов, создание и жизненный цикл
прогона, загрузка вложений): требует confirmation-фреймворка
(
prepare → confirm → execute), которого пока нет ни у одного провайдера.
В каталоге нет ни одной операции со словом prepare.
- Проверка CIDR для приватных адресов: границей исходящих направлений
служит список инсталляций оператора, а не проверка диапазонов RFC1918 — у
on-prem TMS внутренний адрес штатный, а ограничение egress остаётся задачей
сетевой политики стенда.
- Контрактные тесты: утверждения о поведении устаревших GET-списков, о
содержимом ответа списка прогонов и о регистре query-параметров — выводы из
OpenAPI-спеки официального клиента, а не измерения живой инсталляции
(Cloud и on-prem). Список вопросов — в спецификации.
- Кэш, circuit breaker, семантический индекс и webhooks: не реализованы;
кэша нет вовсе, поэтому и межпользовательской утечки через него быть не
может.
Сервисные токены (managed service credentials)
Развёртывание может завести один read-only сервисный токен и разрешить подключениям работать от его имени: выпустить personal access token может не каждый — у подрядчика нет своей учётной записи в корпоративном GitLab, стажёру не выдали прав на токен, — а один централизованно сменяемый аккаунт снимает с пользователей и ротацию, и вопрос «чей это был доступ». Владелец токена — развёртывание: пользователь не может ни создать профиль, ни сменить секрет, ни выбрать профиль по id. Возможность реализована у всех семи интеграций; классификация service-safe операций у каждой своя, и чувствительные чтения каждого провайдера перечислены в его разделе.
managedServiceCredentials:
enabled: true
defaultForNewConnections: true
rateLimit: # потолок сервисного режима; 0 отключает измерение
perPrincipal:
requestsPerMinute: 120
perCredential:
requestsPerMinute: 1000
maxConcurrent: 16
profiles:
- id: gitlab-corp-readonly
provider: gitlab
instance: corp # id из gitlab.instances
label: QA GitLab Read-only # псевдоним, который видит пользователь
credential:
type: pat
secretFile: /run/secrets/qa_gitlab_service_token
resources:
projects:
- "1208"
- group/product
groups:
- group/platform
policy:
gitlab.issues.read: deny # запрет раздела имеет приоритет над разрешением операции
jobs.log: allow # явно разрешить чтение логов в пределах resources
- id: teamcity-readonly
provider: teamcity
instance: teamcity # сервер TeamCity один, id фиксирован
label: QA TeamCity Read-only
credential:
type: personal_access_token
secretEnv: QA_TEAMCITY_SERVICE_TOKEN
resources:
projects:
- PLATFORM
enabled по умолчанию false: пока развёртывание не включило срез, о сервисном токене нет ни строчки — ни чекбокса в форме подключения, ни профилей, ни нового поведения. defaultForNewConnections решает, с какого режима начинается новое подключение. resources обязателен и проверяется при загрузке: профиль без границы — это общий аккаунт, который видит всё, что видит его владелец, и ровно этого возможность не допускает. Записи resources — те идентификаторы, которые принимает сам провайдер, и словарь границы у каждого свой: projects (id проекта GitLab или полный путь, путь группы, id проекта TeamCity, Jira и Test IT, слаг проекта Weblate), spaces (ключи пространств Confluence) и portals (хост портала Bitrix24 — единственная граница, потому что Bitrix24 не дробит доступ по проектам). Секрет берётся из смонтированного файла (secretFile) или из переменной окружения (secretEnv) — ровно один из двух, в YAML он не лежит; у Bitrix24 секрет — полный URL сервисного входящего вебхука, портал брокер выводит из него сам. Опечатка в профиле роняет загрузку конфига, а не выбрасывает профиль молча: сообщение называет профиль или инстанс и говорит, что именно не так, — неизвестный провайдер, неизвестный инстанс, инстанс, занятый двумя профилями, или resources, не назвавший ни одного вида ресурсов.
Новое подключение начинается на сервисном токене, когда профиль для выбранного инстанса есть и defaultForNewConnections включён (по умолчанию так): чекбокс «Использовать сервисный токен» в форме уже отмечен, поля токена нет вовсе, а кнопка называется «Подключить сервисный токен». Снятая галочка возвращает обычную форму с personal access token. У уже существующих подключений режим не меняется: он хранится в самом подключении, поэтому обновление стенда не переводит пользователя на общий аккаунт само. В подключённой карточке режим виден строкой «Сервисный аккаунт · управляется администратором», а рядом — «Изменить доступ» и «Использовать личный аккаунт».
Политика сервисного профиля настраивается в админке, в разделе «Операции сервисного аккаунта». Для каждой операции можно выбрать «Разрешить», «Запретить» или вернуть поведение по умолчанию. Настройка общая для всех пользователей выбранного профиля и применяется к уже созданным подключениям. Без явного разрешения сохраняются прежние ограничения на чувствительное чтение. Например, builds.log: allow открывает лог сборки TeamCity, а jobs.log: allow — лог джоба GitLab; флаг соответствующего провайдера также должен быть включён.
policy принимает allow и deny для имени операции (builds.log), возможности (logs.read) или ключа с префиксом провайдера (teamcity.logs.read). Если подходят несколько правил, любой deny имеет приоритет. Выбор пользователя может дополнительно закрыть раздел, но не расширяет разрешение профиля. В карточке подключения отображается актуальное состояние, включая частичный доступ к разделам, в которых разрешены не все операции.
Разрешение чтения сохраняет границу resources, ограничения ответа, маскирование секретов, аудит и лимиты запросов. Провайдер сначала доказывает принадлежность ресурса разрешённому проекту или пространству, и только затем отдаёт содержимое. Операции, для которых принадлежность ресурса установить нельзя (например, глобальное вложение Test IT без связи с проектом), остаются доступными через личное подключение; админка объясняет это рядом с операцией. Запись, админ-действия, секретные данные, произвольный REST и неклассифицированные операции через сервисный аккаунт не разрешаются. Права сервисного токена на стороне внешней системы этого правила не меняют.
Граница ресурсов нужна потому, что сервисный аккаунт видит больше, чем один пользователь. В сервисном режиме провайдер сверяет с allowlist каждый вызов: проект, названный по id или по пути, должен быть в списке или под перечисленной группой; сборка TeamCity, названная по id, сначала разрешается в свой проект; страница Confluence, названная по id, — в своё пространство; задача Jira и запись Test IT — в свой проект; компонент Weblate — в свой проект-слаг; вебхук Bitrix24 обязан отвечать на портале, названном в границе, — других идентификаторов у границы этого провайдера нет. Листинг, не называющий ресурс, отказывается, а не отвечает всей картиной, доступной общему аккаунту. Списки (gitlab_projects_list, teamcity_projects, projects.list Weblate, projects.list Test IT) собираются из allowlist, а не из того, что видит сервисный токен: ресурс, которого токен не видит, приходит в unavailableResources, а не исчезает молча, — это и есть ответ оператору, что граница и токен разошлись. Разворачивая группу GitLab, провайдер просит только её собственные проекты (with_shared=false) и дополнительно сверяет каждый вернувшийся проект с границей: чужой проект, расшаренный в группу, в выдачу не попадает. Пользователь может сузить список в карточке кнопкой «Изменить доступ», но не расширить: выбор вне администраторского списка отбрасывается и не сохраняется.
Режим — одно из двух явных значений, personal или service; auto не существует. Отказ в личном режиме (403 от GitLab или TeamCity) — это отказ: его не повторяют сервисным токеном, и наоборот тоже. Если личный токен был сохранён раньше, он остаётся зашифрованным в хранилище, но не тратится, пока подключение работает на сервисном; «Использовать личный аккаунт» возвращает его, а если личного токена не сохраняли, переключение отвечает PersonalCredentialRequired вместо подключения без личности. Переподключение в любую сторону поднимает binding revision — и смена режима, и повторное сохранение того же самого профиля, — поэтому всё, что было выведено из прежней личности, с этого момента недействительно: кэш, курсоры, подготовленные действия и вердикт проверки, который не успел вернуться. Опоздавший вердикт сверяется с поколением связи, из которого начался проб (строка связи, её ревизия, ссылка на секрет и сервисный профиль вместе), и при несовпадении отбрасывается с логом credential.validation-stale, а не перезаписывает живое подключение: транзакция вокруг одного UPDATE этого не закрывает, потому что разрыв — это await перед записью, а не сама запись. Если к моменту записи связи нет вовсе (её отключили и не завели заново), тот же отказ пишется отдельным ключом credential.validation-unbound, чтобы человек, разбирающий потерянную гонку, читал из журнала два разных диагноза, а не один на два случая. Ревизии для этой сверки недостаточно: disconnect стирает строку, а следующее connect заводит новую с ревизией, отсчитывающейся заново от единицы, — и у сервисного подключения без личного токена в новый строке совпадают все остальные поля поколения (секрета нет, тот же профиль, та же ревизия), так что вердикт, начатый до отключения, приземлился бы на здоровое подключение и выставил бы ему status=error с чужим last_error_code. Поэтому сверяется и id строки: опоздавший ответ не имеет права коснуться связи, которой не застал. Тот же разрыв закрывается на пути переключения режима — личное направление сначала прощупывает сохранённый токен наверху, а потом пишет найденную личность и грант, и переподключение внутри этого ожидания приземлило бы на живую связь личность уже заменённого токена; такая запись тоже сверяется с поколением, при несовпадении отбрасывается с логом credential.switch-stale (или credential.switch-unbound, если связь к этому моменту удалена), а пользователю отвечает IntegrationNotConnected — связью, которая уже ушла, — и не записывает в журнал успешно совершённым переключением, которого не было.
Секрет читается лениво, из файла или из переменной окружения, и кэшируется по отпечатку: ротация смонтированного секрета действует со следующего вызова — никому не нужно переподключаться и хост не нужно перезапускать, а содержимое секрета хешируется в ревизию credential. Сам секрет не пишется в лог, не возвращается в браузер и не рендерится: карточка показывает администраторский псевдоним профиля. Пропавший или пустой секрет — ServiceCredentialUnavailable (профиль не обслуживает ничего, пока файл не появится), не подходящий провайдеру — ServiceCredentialInvalid, выключенный профиль — ServiceCredentialDisabled. Проверка подключения читает вердикт пробы: истёкший или отозванный токен переводит подключение в «нужна проверка» и отвечает отказом, а слишком широкий токен (ServiceCredentialUnsafeScope) подключение не ломает — потолок режима уже ограничивает доступ, — но карточка о нём говорит, потому что сузить токен может только оператор. Остальные отказы карточка переводит на человеческий: ServiceResourceNotAllowed — «сузьте доступ в списке или подключите личный аккаунт», SensitiveReadRequiresPersonalCredential — «нужен личный аккаунт», OperationNotAllowedWithServiceCredential — «сервисный режим только для безопасного чтения».
Аудит в сервисном режиме — единственная запись о том, кто на самом деле спросил: наверху виден только сервисный аккаунт, поэтому каждая строка журнала хранит аутентифицированного пользователя QA, провайдера, операцию, credential_source и id профиля. Пишутся и отказы — по потолку и по границе, — иначе запрос, который upstream не может отличить от чужого, не оставил бы следа. Секрет не попадает ни в журнал, ни в результат инструмента.
Rate limiting сервисного режима считается по обеим сторонам сразу: rateLimit.perPrincipal.requestsPerMinute — сколько запросов в минуту одному пользователю на провайдера, rateLimit.perCredential.requestsPerMinute — сколько выдерживает общий токен, и perCredential.maxConcurrent — сколько вызовов на нём разрешено держать в полёте. Пороги расходуются непрерывно, а не сбрасываются по минуте: всплеск съедает минуту вперёд, дальше идёт по равномерной скорости. Проверки читаются до списания, поэтому отказ всегда по одному основанию: вызов, которому не хватило общего бюджета, не тратит личный, и наоборот — иначе пользователь, упирающийся в общий потолок, платил бы за него собственным будущим бюджетом. Личный режим не ограничивается: частоту там задаёт сам сервер. Отказ отвечает RateLimited, в аудите записывается как denied с реальным пользователем и наверх не уходит; лимит живёт в этом процессе, где стоит единственный путь к сервисному токену. 0 отключает измерение. Межвызовных кэшей результатов в плагине нет ни у одного провайдера, поэтому переключение режимов не может отдать устаревшие данные.
Граница доверия
Интеграционный principal получается только из QA Surface:
- браузер аутентифицируется штатным account token QA Surface;
secureSession проверяет владельца DSH-сессии;
- интеграционный principal закрепляется только если текущий пользователь и владелец совпадают;
- администраторский просмотр чужого чата, неаттестованная сессия и subagent не получают principal;
- broker находит подключение по паре
ownerUserId + provider и только внутри себя расшифровывает credential — по ссылке того поколения связи, с которого операция началась, так что переподключение, случившееся во время чтения, не тратит чужой текущий токен; ссылка, которую живая строка уже не несёт, отвечает IntegrationNotConnected, а не PersonalCredentialRequired, и не становится отказом политики;
- адрес внешнего сервиса приходит только из конфига оператора: у GitLab, Jira, Confluence, Test IT и Weblate это выбранная инсталляция из списка оператора, у Bitrix24 — проверенный по доменному allowlist URL вебхука, у TeamCity — адрес, проверенный по политике стенда (и при сохранении, и на каждом вызове);
- у Jira модели не достаётся и язык запросов: JQL собирается из типизированных аргументов внутри провайдера, а значения экранируются как строковые литералы; у Weblate так же собирается
q, и next-ссылка разбирается на номер страницы, а не берётся как адрес следующего запроса;
- справочник пользователей Jira читается только для того, чтобы превратить имя из фильтра в accountId: ответ справочника не попадает ни в результат тула, ни в лог, а неоднозначное имя — отказ, а не выбор человека за пользователя;
- в сервисном режиме личность на стороне провайдера принадлежит развёртыванию, а не пользователю: подключение тратит один общий read-only аккаунт, и ни один запрос не уходит от имени вошедшего;
- локальный audit — единственная запись о том, кто спросил на самом деле: upstream видит только сервисный аккаунт, поэтому строка журнала хранит владельца подключения, provider, operation,
credential_source и id сервисного профиля;
- неявного отката нет ни в одну сторону: сервисный режим не подменяет личный после отказа и наоборот, а любое переподключение — смена режима или повторное сохранение того же профиля — поднимает binding revision и обесценивает всё, что было выведено из прежней личности; опоздающая запись (вердикт проверки и переключение режима, чья проба успела сходить наверх) сверяется с этим поколением вместе с id строки — отключение стирает связь, а следующая за ним ревизия начинается заново с единицы, так что без id свежее подключение не отличить от того, которое отключили, — и при несовпадении отбрасывается (
credential.validation-stale и credential.switch-stale, когда связь уже несёт другое поколение; credential.validation-unbound и credential.switch-unbound, когда её к этому моменту нет вовсе), а не перезаписывает живое подключение и не остаётся в журнале успешно совершённым;
В логах и audit сохраняются provider, operation, result и source session id; в audit к этому добавлены владелец подключения, credential_source и id сервисного профиля — у личного подключения это personal и null. Тела CRM/чатов, HTTP-заголовки и секреты не журналируются. Постоянный cache данных провайдера отсутствует, поэтому и смена режима не может отдать данные, прочитанные под прежней личностью.
Подсказки к credential
Рядом с полем секрета карточка показывает, откуда этот секрет берётся: где его выпускают, какая документация описывает авторизацию, какие права нужны и что именно вставлять в поле. Текст раскрывается по клику в панель, а не занимает карточку целиком; формулировка зависит от механизма — «Создать API-ключ», «Создать токен», «Войти и авторизовать», «Как это настроить» для подключения без страницы выдачи (вебхук Bitrix24).
Подсказка — это метаданные, а не секрет. В ней нет ни значения credential, ни его снимка, ни результата авторизации; браузер получает её тем же RPC qaIntegrations/providers, что и список провайдеров, уже после входа в QA-аккаунт. Она не является зависимостью подключения: если метаданных нет, карточка показывает обычное поле секрета, а сломавшаяся ссылка не мешает ни сохранить, ни проверить credential.
Метаданные объявлены рядом с провайдером — src/providers/<id>/credential-help.ts — и не хранятся центральным списком, поэтому новая интеграция приносит свою подсказку вместе с собой. Ссылки проверяются по форме, без обращения к сети: допускаются только https: и — для локальных, приватных и self-hosted адресов — http:; javascript:, data:, file: и адрес с credential внутри отклоняются, а внешние ссылки открываются с noopener noreferrer.
Развёртывание может заменить подсказку целиком или по полям: корпоративный GitLab, Jira Data Center, внутренняя вики и прокси-шлюзы попадают в тот же UI, что и облачные сервисы.
credentialHelp:
gitlab:
obtainUrl: https://gitlab.example.internal/-/user_settings/personal_access_tokens
obtainLabel: Выпустить токен
docsUrl: https://wiki.example.internal/dsh/gitlab
instructions: Откройте внутреннюю страницу токенов.
selfHosted: true
jira:
enabled: false # спрятать подсказку; поле секрета остаётся
weblate:
docsLabel: Наша вики
Оверрайд действует на всю инсталляцию и не меняет сам механизм подключения: невалидный адрес скрывает только свою ссылку, а в лог при старте попадает предупреждение credential-help.override с именем провайдера и поля. Неизвестный kind деградирует до custom, а не ломает провайдера. Значения по умолчанию для каждого провайдера и порядок добавления новой подсказки — в src/providers/README.md.
Подготовка master key
Создайте отдельный 256-битный ключ вне БД и репозитория. Docker secret по умолчанию должен быть доступен Host как:
/run/secrets/qa_integrations_master_key
Файл может содержать ровно 32 случайных байта, 64 hex-символа или base64 от 32 байт. Пример для Linux:
umask 077
openssl rand -base64 32 > qa_integrations_master_key
Пример для PowerShell:
$keyBytes = [Security.Cryptography.RandomNumberGenerator]::GetBytes(32)
[Convert]::ToBase64String($keyBytes) | Set-Content -NoNewline qa_integrations_master_key
Не меняйте файл ключа поверх существующего deployment: старые записи перестанут расшифровываться. KeyProvider и SecretStore.rewrap() поддерживают контролируемую ротацию с одновременной доступностью старой и новой версии; операционный CLI ротации оставлен для hardening-фазы.
Конфигурация
Плагин безопасно выключен в bundle patch, пока не смонтирован master key:
- update:
id: qa-integrations
config:
enabled: true
dataPath: /var/lib/dsh/qa