DeepSeek Harness Plugin Hub

发布与管理完整 Harness Profiles,发现适合你的插件。

探索

插件目录环境预设文档中心动态

社区

发布插件联系我们报告问题

相关链接

Plugin Hub GitHubDeepSeek Harness 官方项目系统状态隐私说明
© 2026 DeepSeek Harness Plugin HubPowered byPaxTech

独立、非官方社区项目,与 DeepSeek 官方无隶属、授权或背书关系。

DeepSeek Harness Plugin Hub
ProfilesPlugins分类动态文档登录管理 Profiles
ProfilesPlugins分类动态文档登录
← Plugins

@yadsh/dsh-qa-integrations

Qa Integrations

面向 DSH QA Surface 的按主体范围划分、加密的用户集成

插件会安装到这里;不确定时保持 web。

npx -y @deepseek-ai/dsh plugin --profile web add @yadsh/dsh-qa-integrations@0.11.0
README兼容性版本

兼容性与来源证明

Qa Integrations 以 @yadsh/dsh-qa-integrations 发布,当前版本为 0.11.0。Plugin Hub 会校验它的 manifest,并保存精确安装来源,便于复现安装结果。

DSH 兼容范围
*
运行环境
web
发布来源
npm
Registry 更新时间
2026/11/4

版本

0.11.0stable
2026/10/27
0.10.5stable
2026/10/24
0.10.4stable
2026/10/11
查看其余 12 个版本收起版本
0.10.3stable
2026/10/8
0.10.2stable
2026/10/8
0.10.1stable
2026/10/5
0.10.0stable
2026/10/4
0.9.0stable
2026/9/24
0.8.3stable
2026/9/23
0.8.2stable
2026/9/22
0.8.1stable
2026/9/22
0.8.0stable
2026/9/22
0.7.0stable
2026/9/21
0.6.0stable
2026/9/18
0.5.0stable
2026/9/17
最新版
0.11.0
DSH
*
HMR
重启进程
Tree shaking
未声明可安全裁剪
解包体积
6.3 MB
文件数
402
Surface
web
许可证
MIT
发布源
npm
GitHub
★ 3
周下载
180
最近提交
2026/10/27
查看源码 ↗项目主页 ↗
README Badge

点击下方 Badge 复制 Markdown,粘贴到 README 即可。

这是你的 Plugin?认领权益 · 优先安全扫描

验证 package.json 声明的 GitHub 仓库,即可管理这个公开页面。认领后,Hub 会优先安排当前版本的安全扫描,并在通过后公开展示结果。

认领这个 Plugin →
报告问题

README

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_listMerge 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.readcrmCRM, дела, таймлайн, товарные строки, дубли
crm.comment.writecrmЕдинственная операция записи: комментарий в таймлайн лида, сделки, контакта или компании
chat.readimЧаты, сообщения, поиск по переписке
openlines.readimopenlinesДиалоги открытых линий и их история
user.readuser_brief / user_basic / userСвой профиль и поиск сотрудников
department.readdepartmentСтруктура компании
tasks.readtaskЗадачи
calendar.readcalendarКалендарь и занятость
disk.readdiskФайлы Диска

У 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.readread_user / read_api / apiСвой профиль: логин, имя, инстанс
projects.readread_api / apiПроекты, видимость, ветка по умолчанию
repository.readread_repository / read_api / apiДерево, файлы, коммиты, сравнение веток
search.readread_api / apiПоиск по проектам, задачам, MR, коммитам, коду
issues.readread_api / apiЗадачи и комментарии к ним
merge_requests.readread_api / apiMR, диффы, обсуждения, одобрения, пайплайны MR
ci.metadata.readread_api / apiПайплайны, джобы, их статусы и метаданные
ci.logs.readread_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:

  1. браузер аутентифицируется штатным account token QA Surface;
  2. secureSession проверяет владельца DSH-сессии;
  3. интеграционный principal закрепляется только если текущий пользователь и владелец совпадают;
  4. администраторский просмотр чужого чата, неаттестованная сессия и subagent не получают principal;
  5. broker находит подключение по паре ownerUserId + provider и только внутри себя расшифровывает credential — по ссылке того поколения связи, с которого операция началась, так что переподключение, случившееся во время чтения, не тратит чужой текущий токен; ссылка, которую живая строка уже не несёт, отвечает IntegrationNotConnected, а не PersonalCredentialRequired, и не становится отказом политики;
  6. адрес внешнего сервиса приходит только из конфига оператора: у GitLab, Jira, Confluence, Test IT и Weblate это выбранная инсталляция из списка оператора, у Bitrix24 — проверенный по доменному allowlist URL вебхука, у TeamCity — адрес, проверенный по политике стенда (и при сохранении, и на каждом вызове);
  7. у Jira модели не достаётся и язык запросов: JQL собирается из типизированных аргументов внутри провайдера, а значения экранируются как строковые литералы; у Weblate так же собирается q, и next-ссылка разбирается на номер страницы, а не берётся как адрес следующего запроса;
  8. справочник пользователей Jira читается только для того, чтобы превратить имя из фильтра в accountId: ответ справочника не попадает ни в результат тула, ни в лог, а неоднозначное имя — отказ, а не выбор человека за пользователя;
  9. в сервисном режиме личность на стороне провайдера принадлежит развёртыванию, а не пользователю: подключение тратит один общий read-only аккаунт, и ни один запрос не уходит от имени вошедшего;
  10. локальный audit — единственная запись о том, кто спросил на самом деле: upstream видит только сервисный аккаунт, поэтому строка журнала хранит владельца подключения, provider, operation, credential_source и id сервисного профиля;
  11. неявного отката нет ни в одну сторону: сервисный режим не подменяет личный после отказа и наоборот, а любое переподключение — смена режима или повторное сохранение того же профиля — поднимает 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

README 内容较长,此处已截断,完整内容请查看源码。

gitlab_pipeline_jobs_list
Джобы пайплайна: стадия, статус, длительность
gitlab_job_getОдна джоба: причина падения, теги, список артефактов
gitlab_job_log_getЛог джобы: ограничен по размеру и очищен от токенов
weblate_screenshot_getМетаданные одного скриншота: имя, файл в репозитории, язык, id связанных строк
WAS … CHANGED BEFORE …
DURING
after
before
include: ["changelog_summary"]
customfield_…
*all
ResultTooLarge
  • Кэш (§Caching) отсутствует полностью, поэтому и межпользовательской утечки через кэш быть не может: схема полей перечитывается на каждый такой вызов, а курсор поиска — это continuation-токен Jira, а не наше состояние.
  • Rate limiting (§Rate limiting): в личном режиме лимитов на пользователя и на сайт нет; вместо них — ограниченные страницы, таймаут стенда (timeoutMs) и повторы с Retry-After для 429/5xx. Инвалидация по webhooks (§Future work) — тоже не подключена.
  • testit_configurationsКонфигурации проекта, в разрезе которых считаются результаты
  • reach подключения — это пересечение сохранённого гранта с тем, что провайдер предлагает этому развёртыванию сейчас: возможность, снятую конфигурацией провайдера или его каталогом операций, брокер отказывает до расшифровки секрета и до первого байта наверху; карточка в тот же момент перестаёт показывать её как доступную — строка возможности остаётся в списке «Доступ агента», потому что список строится по каталогу провайдера, а не по живому набору грантов, но чекбокс у неё выключен и в списке политик возможности нет, — а новая запись allow на неё не заводится: иначе строка, простоявшая в окне снятия, ожила бы сама, когда возможность вернут, вопреки правилу «новое начинается с deny».