@yadsh/dsh-qa-integrations
v0.10.3
Published
Principal-scoped, encrypted user integrations for DSH QA Surface
Maintainers
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_list | Merge requests по проекту, веткам, автору, ревьюеру, черновику |
| gitlab_merge_request_get | Карточка MR: описание, ветки, статус слияния, конфликты |
| gitlab_merge_request_changes_get | Изменённые файлы MR с диффами |
| gitlab_merge_request_discussions_list | Обсуждения ревью и их разрешение |
| gitlab_merge_request_approvals_get | Сколько одобрений нужно, сколько есть, кто одобрил |
| gitlab_merge_request_pipelines_list | Пайплайны, привязанные к MR |
| gitlab_pipelines_list | Пайплайны проекта по ветке, статусу, источнику, автору |
| gitlab_pipeline_get | Один пайплайн: статус, длительности, покрытие |
| gitlab_pipeline_jobs_list | Джобы пайплайна: стадия, статус, длительность |
| gitlab_job_get | Одна джоба: причина падения, теги, список артефактов |
| gitlab_job_log_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_screenshot_get | Метаданные одного скриншота: имя, файл в репозитории, язык, id связанных строк |
Списочные инструменты Weblate отвечают конвертом { items, pagination }: pagination.nextPage — номер следующей страницы, а не подписанный курсор. По ссылке next провайдер не ходит сам: Weblate кладёт в тело абсолютный адрес, из которого читается только номер страницы, и только когда он ведёт на настроенный инстанс — ссылка с чужого адреса отбрасывается, а список честно считается законченным. Страница — 20 строк по умолчанию, а perPage сверх потолка стенда (maxPageSize, по умолчанию 100) обрезается, а не отвергается: «дай больше строк» — это запрос, на который стенд отвечает своим потолком, а не ошибка, которую модель выясняет перебором. Состояние между вызовами не хранится, principal и права перепроверяются на каждом вызове.
Модель не получает q: фильтры типизированные (source, target, context, state, failingChecks, suggestions, comments, а у поиска по всему инстансу ещё project, component и language), а строку запроса собирает query.ts — значения всегда в двойных кавычках с экранированием \ и ", поэтому кавычка или обратный слэш внутри запроса остаются значением и не становятся условием. Словарь состояний — собственный словарь Weblate is:: untranslated (по определению самого Weblate — всё, что ниже translated, то есть вместе со строками, помеченными needs-editing), needs-editing, translated, approved, read-only. Поиск без единого фильтра уходит без параметра q, а не с пустым. weblate_failing_units_list — тот же /units/, что и weblate_units_find, с жёсткой клаузой has:check; Weblate сообщает, что строка не проходит проверку, но не то, какая именно проверка упала, поэтому ответ несёт hasFailingCheck, а не список имён проверок.
Строки нормализованы: числовые состояния Weblate (0/10/20/30/100) отображаются в untranslated/needs-editing/translated/approved/read-only, формы множественного числа остаются массивом (перевод, молча короче настоящего, был бы прочитан как правда), текст режется — 200 символов в списке, maxChars у отдельной строки, потолок — maxTextChars стенда — и помечается textTruncated, метки приходят именами, а проект, компонент и язык читаются из вложенной ссылки translation, и только когда она ведёт на настроенный инстанс. Каждый ответ с текстом, написанным на стороне Weblate, помечен untrustedExternalContent: true: это данные, а не инструкция. Скриншоты — только метаданные (имя, файл в репозитории, язык, id строк, адрес картинки на настроенном инстансе): байты провайдер не скачивает. История изменений у Weblate есть у проекта, компонента и перевода, но не у строки, поэтому инструмент проектный, а строка в каждой записи ищется по её id.
weblate_connection_get называет инстанс, аккаунт и вид токена: у Weblate нет эндпоинта «кто я», и подключённый аккаунт определяется по GET /api/users/, который непривилегированный токен видит одной своей строкой. Токен, который умеет перечислять пользователей, отвечает полным списком — тогда аккаунт честно не определяется (пустой внешний id, в карточке адрес инстанса и вид токена), а не угадывается по первой строке.
Возможности и скопы Confluence
Подключение — Atlassian API token вместе с почтой аккаунта Atlassian (Basic-пара). Возможность появляется у пользователя, если её включил оператор (confluence.*Read).
| Возможность | Что открывает |
| ------------------ | ------------------------------------------------------------- |
| identity.read | Сайт Confluence и подключённый аккаунт Atlassian |
| spaces.read | Список пространств и карточка пространства |
| search.read | Поиск страниц по тексту, пространствам, меткам, автору и дате |
| content.read | Страница целиком с текстом, метками и версией |
| comments.read | Комментарии страницы: подвал, inline и ответы |
| attachments.read | Метаданные вложений страницы |
| versions.read | История версий страницы |
Confluence, как и TeamCity, не сообщает через API, какие права выданы конкретному токену: у OAuth-приложений для этого есть /oauth/token/accessible-resources, а у API token аналога нет. Поэтому сужение идёт только по деплой-флагам оператора, а права аккаунта применяет сам Confluence — отказ приходит как ProviderPermissionDenied, и провайдер никогда не пробует другой credential.
Оператор дополнительно может сузить пространства: confluence.allowedSpaces — это список ключей, и он применяется одинаково к поиску, к списку пространств и к прямому чтению страницы, комментариев, вложений и версий. Страница в пространстве вне списка отвечает отказом OperationDeniedByPolicy, а не тихо исчезает из выдачи; в поиск добавляется space in (…), и строки, пространство которых по ответу не подтверждается, отбрасываются. Страница не несёт ключ пространства, только spaceId, поэтому каждое чтение страницы делает дополнительный запрос самого пространства — он же и наполняет ответ ключом и именем, и по нему принимается решение политики; чтения комментариев, вложений и версий с включённой политикой стоят ещё одного чтения страницы, чтобы узнать её пространство.
Скопы, которые нужны токену: классические read:confluence-content.all, read:confluence-space.summary, search:confluence, read:confluence-user (или гранулярные read:page:confluence, read:comment:confluence, read:attachment:confluence, read:space:confluence, read:content.metadata:confluence, read:user:confluence). Классический токен работает по адресу сайта (https://company.atlassian.net); токен со скоупами Atlassian принимает только через шлюз https://api.atlassian.com/ex/confluence/{cloudId} — такой адрес оператор может задать как instances[].baseUrl, и провайдер будет ходить по тому же относительному пути /wiki/.... Сам cloudId провайдер не ищет. Для Server / Data Center скоупов и почты нет: там нужен личный токен доступа, а адрес инстанса задаётся вместе с его контекстным путём (https://wiki.example.corp/confluence).
Запись в surface отсутствует: создание и правка страниц, комментарии, резолв inline-комментариев, вложения, перемещение и администрирование пространств ждут confirmation-фреймворка. Произвольного REST (confluence_rest_call), произвольного CQL и скачивания вложений тоже нет — только метаданные и ссылки, которые отдал сам Confluence.
Сервисный токен Confluence
Подключение может работать от сервисного токена развёртывания: профиль называет сайт, секрет — та же Basic-пара email:token, а граница ресурсов — spaces (ключи пространств). Чувствительных чтений в каталоге нет: поиск, пространства, страницы, комментарии, метаданные вложений и версии остаются сервисно-безопасными, но каталог не умеет отдавать байты вложений, и такая операция, когда появится, обязана прийти с классификацией не ниже чувствительной. Страница, названная по id, сначала раскрывается в своё пространство, поиск сужается до space in (…) по границе ещё на входе, а строки, пространство которых по ответу не подтверждается, отбрасываются вторым замком. Граница сервиса и операторский confluence.allowedSpaces — два независимых сужения: сервисный вызов живёт в их пересечении.
Возможности и скопы Weblate
Подключение — API-токен Weblate вместе с выбором инстанса: адреса задаёт только оператор (weblate.instances), пользователь выбирает инстанс из списка и вставляет токен. Возможность появляется у пользователя, если её включил оператор (weblate.*Read).
| Возможность | Что открывает |
| ------------------- | ----------------------------------------------------------------- |
| identity.read | Инстанс Weblate, подключённый аккаунт и вид токена |
| projects.read | Список проектов и карточка проекта |
| components.read | Компоненты проекта и карточка компонента |
| translations.read | Языки компонента и один язык целиком |
| units.read | Поиск строк по тексту, контексту и состоянию, чтение одной строки |
| checks.read | Строки, у которых не проходит хотя бы одна проверка |
| comments.read | Комментарии к строке |
| suggestions.read | Предложенные переводы строки |
| changes.read | История изменений проекта |
| statistics.read | Готовность проекта, компонента и языка |
| screenshots.read | Метаданные скриншотов |
Weblate, как и TeamCity с Confluence, не сообщает через API, какие права выданы конкретному токену: токен действует от имени своего пользователя или своего проекта, а «проверить» чтением чужой области провайдер не имеет права. Поэтому сужение идёт только по деплой-флагам оператора (weblate.*Read), а права применяет сам Weblate — отказ приходит как ProviderPermissionDenied, и провайдер никогда не пробует другой credential. Каждая возможность при этом достижима хотя бы одной операцией, поэтому карточка не может показать переключатель, который ничего не делает.
Префиксы токенов Weblate (wlu_ — личный, wlp_ — проектный) доходят до пользователя подписью в карточке (· личный токен, · токен проекта, · токен) и никогда не являются проверкой прав: токен с незнакомым префиксом принимается наравне с остальными, а его права определяет сам Weblate. Проектный токен карточка рекомендует как меньший радиус поражения — это подсказка, а не запрет.
Оператор стенда дополнительно решает, какие из инструментов видит модель: имена weblate_* нужно добавить в tool allow-list пресета QA. Инструменты не принимают ни пользователя, ни credential, ни инстанс, поэтому допуск к ним — решение оператора, а не модели.
Запись в surface отсутствует: предложения перевода, комментарии, правка и утверждение перевода, файлы перевода, autotranslate и операции с репозиторием ждут confirmation-фреймворка. Инструмента с произвольным q и произвольным REST тоже нет: поиск принимает только типизированные фильтры.
Возможности и скопы
Одна возможность = один scope Bitrix24. Возможность появляется у пользователя только если её включил оператор (bitrix24.*Read, у записи — bitrix24.crmCommentWrite) и подключённый вебхук реально получил соответствующий scope — плагин спрашивает это у портала методом scope при подключении и при нажатии «Проверить».
| Возможность | Scope вебхука | Что открывает |
| ------------------- | ------------------------------------ | ---------------------------------------------------------------------------------------- |
| crm.read | crm | CRM, дела, таймлайн, товарные строки, дубли |
| crm.comment.write | crm | Единственная операция записи: комментарий в таймлайн лида, сделки, контакта или компании |
| chat.read | im | Чаты, сообщения, поиск по переписке |
| openlines.read | imopenlines | Диалоги открытых линий и их история |
| user.read | user_brief / user_basic / user | Свой профиль и поиск сотрудников |
| department.read | department | Структура компании |
| tasks.read | task | Задачи |
| calendar.read | calendar | Календарь и занятость |
| disk.read | disk | Файлы Диска |
У Bitrix24 нет read-only scope вебхука: crm покрывает и чтение, и запись, поэтому гранулярность «запись отдельно» даёт только конфиг-флаг. crm.comment.write выключен по умолчанию; даже после включения флага операция стартует с политикой deny, пока пользователь (или оператор) явно её не разрешит, — проба scope сама по себе запись не открывает. Остальная запись (*.add, *.update, *.delete), бизнес-процессы, управление пользователями и generic REST/MCP в surface отсутствуют.
Выданные в Bitrix24 права не отменяются: портал всё равно применяет права того пользователя, чей вебхук используется. Возможность, обнаруженная после выдачи нового scope, появляется в карточке выключенной — пользователь включает её сам.
Сервисный токен Bitrix24
Подключение может работать от сервисного вебхука развёртывания: профиль managedServiceCredentials называет портал (хост, в любом регистре — он сравнивается с хостом, который брокер выводит из URL вебхука), секретом служит полный URL сервисного входящего вебхука, а граница ресурсов — единственный вид portals: сервисный вызов обязан отвечать на портале из списка, других идентификаторов у границы нет. Проба (validateServiceCredential) читает только profile и scope; подтвердить read-only она не может — scope crm и читает, и пишет одним битом, — поэтому вердикт всегда «здоров» с предупреждением, а потолок режима обеспечивают классификация операций и граница. Сервисному токену недоступны по классификации: запись в таймлайн, справочник сотрудников (телефоны и почта), дубликаты по телефону и почте, расшифровки звонков, все чтения чатов и открытых линий, календарь занятости и Диск — это чтения, ответы которых принадлежат конкретным людям, а не порталу; CRM-записи, задачи и структура отделов остаются сервисно-безопасными.
Возможности и скопы GitLab
Подключение — personal access token. Возможность появляется у пользователя только если её включил оператор (gitlab.*Read) и токен при подключении реально предъявил соответствующий scope: плагин спрашивает scopes у самого токена (GET /api/v4/personal_access_tokens/self) при сохранении и при нажатии «Проверить». Токен, который не может прочитать себя (старый GitLab, group/project access token), только снижает точность — срез остаётся на деплой-флагах оператора.
| Возможность | Scope токена | Что открывает |
| --------------------- | -------------------------------------- | ---------------------------------------------- |
| identity.read | read_user / read_api / api | Свой профиль: логин, имя, инстанс |
| projects.read | read_api / api | Проекты, видимость, ветка по умолчанию |
| repository.read | read_repository / read_api / api | Дерево, файлы, коммиты, сравнение веток |
| search.read | read_api / api | Поиск по проектам, задачам, MR, коммитам, коду |
| issues.read | read_api / api | Задачи и комментарии к ним |
| merge_requests.read | read_api / api | MR, диффы, обсуждения, одобрения, пайплайны MR |
| ci.metadata.read | read_api / api | Пайплайны, джобы, их статусы и метаданные |
| ci.logs.read | read_api / api | Содержимое лога джоба |
CI разделён надвое намеренно: лог джоба — это другая утечка, чем список джоб, а раньше обе половины закрывала одна возможность ci.read. Оператор стенда задаёт половины отдельно (ciMetadataRead и ciLogsRead), а старый флаг ciRead продолжает работать и теперь управляет обеими; сохранённая политика ci.read при первом запуске переносится на оба новых id, поэтому выключенный CI не включается сам. Под сервисным токеном доступна только первая половина: ci.metadata.read отвечает, какие джобы были и чем кончились, а ci.logs.read показывает, что они напечатали, и остаётся личным чтением. Любая операция, читающая проект, в сервисном режиме обязана назвать проект или группу внутри границы профиля, а листинг без проекта (gitlab_issues_list, gitlab_merge_requests_list) отказывается — вместо ответа всей картиной, доступной общему аккаунту.
У GitLab нет отдельного read-scope на каждую область, поэтому read_api покрывает почти всё, а read_repository и read_user дают узкие наборы. Права токена не отменяются: GitLab всё равно применяет права своего пользователя. Возможность, появившаяся после выдачи нового scope, приходит в карточку выключенной. Запись (POST/PUT/DELETE), GraphQL, произвольный REST, admin-API, управление токенами, участниками и CI-переменными в surface отсутствуют; merge и запись в репозиторий — тоже. Сервисный токен не превращает эти запреты в разрешения: он только сужает — и дополнительно прячет от общего аккаунта лог джоба.
Оператор стенда дополнительно решает, какие из инструментов видит модель: имена нужно добавить в tool allow-list пресета QA. Инструменты не принимают ни пользователя, ни credential, поэтому допуск к ним — решение оператора, а не модели.
Возможности и скопы TeamCity
Подключение — персональный access token TeamCity. Адрес сервера задаёт оператор в конфигурации развёртывания (teamcity.serverUrl) — он один на всех, поэтому форма подключения спрашивает только токен, а карточка показывает адрес как справочную строку. Если адрес не задан, карточка говорит об этом вместо формы, которая всё равно не сохранится. Возможность появляется у пользователя, если её включил оператор (teamcity.*Read).
| Возможность | Что открывает |
| --------------------- | ------------------------------------------------------- |
| identity.read | Сервер TeamCity, его версия и подключённый пользователь |
| projects.read | Проекты |
| buildConfigs.read | Конфигурации сборки |
| builds.read | Сборки, карточка сборки, изменения в VCS |
| failures.read | Упавшие тесты и проблемы сборки |
| logs.read | Фрагмент лога сборки |
| queue.read | Очередь сборки |
| investigations.read | Расследования падений |
| agents.read | Агенты сборки |
| artifacts.read | Список артефактов и текст небольших текстовых файлов |
Лог сборки и текст артефакта остаются личными под сервисным токеном: это текст, который напечатала сборка, и общий read-only аккаунт его не читает. Список артефактов — обычное чтение метаданных, но возможность у него одна с текстом, поэтому в сервисном режиме artifacts.read целиком помечена «Требуется личный аккаунт», как и logs.read. В сервисном режиме листинг обязан назвать проект или конфигурацию: teamcity_builds, teamcity_build_configs, teamcity_queue и teamcity_investigations без projectId или buildTypeId отказываются, потому что иначе ответили бы всей картиной сервера, а teamcity_projects собирается из allowlist профиля. Сборка, названная по id, сначала разрешается в свой проект, поэтому прямой teamcity_build проходит ту же проверку, что и листинг. Причины падений (teamcity_build_failures) остаются сервисными: инструмент отвечает упавшими тестами и проблемами сборки, а не логом.
TeamCity, в отличие от GitLab и Bitrix24, не сообщает через API, какие именно права выданы конкретному токену: админ-скоупы невидимы снаружи, а «проверить» записью провайдер не имеет права. Поэтому сужение идёт только по деплой-флагам оператора, а права токена применяет сам TeamCity — отказ приходит как ProviderPermissionDenied, и провайдер никогда не пробует другой credential. Рекомендация пользователю в карточке прямая: создать токен с «Limit per project» и только теми правами чтения, которые нужны.
Запись в surface отсутствует: запуск, перезапуск, отмена сборки, комментарии и теги ждут confirmation-фреймворка; редактирование конфигураций, управление агентами, расследованиями и mute-ами — тоже. Параметры сборки (/parameters, resulting-properties) провайдер не запрашивает вообще, поэтому секретных значений в ответах нет по построению.
Возможности и скопы Jira
Подключение зависит от того, где живёт Jira, и это объявляет оператор в конфигурации развёртывания (jira.sites[].deploymentType). Atlassian Cloud аутентифицирует API-токен Atlassian вместе с e-mail аккаунта через HTTP Basic (email:token); Jira Server / Data Center — личный токен доступа (Personal Access Token) через Bearer, без почты. Провайдер ходит по API того продукта, который объявлен: /rest/api/3 у Cloud, /rest/api/2 у Server / Data Center. Сайт, который отвечает serverInfo.deploymentType другого продукта, отклоняется на подключении с подсказкой, какое значение поставить.
Список сайтов задаёт только оператор — пользователь выбирает сайт из списка и вставляет токен (и почту, если сайт облачный); произвольный хост ввести нельзя. Возможность появляется у пользователя, если её включил оператор (jira.*Read).
| Возможность | Что открывает |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| identity.read | Подключённый пользователь Atlassian и сайт, на котором он работает |
| issues.read | Поиск задач со всем словарём фильтров (включая фильтр по истории поля — WAS/CHANGED — и разрешение имени в идентификатор пользователя) и карточка задачи с описанием, связями и кастомными полями |
| comments.read | Комментарии задачи, включая ограниченные (с пометкой видимости) |
| attachments.read | Метаданные вложений задачи: имя, тип, размер, автор |
| transitions.read | Доступные переходы статуса и их обязательные поля |
| projects.read | Карточка проекта |
| fields.read | Схема полей сайта, включая кастомные, — она же источник id для фильтра по кастомному полю |
Jira, как и TeamCity, не сообщает через API, какие права выданы конкретному API-токену: токен действует от имени пользователя и наследует его права целиком, а «проверить» записью провайдер не имеет права. Поэтому сужение идёт только по деплой-флагам оператора, а права аккаунта применяет сама Jira — отказ приходит как ProviderPermissionDenied (задача, проект или поле закрыты схемой прав, issue security level или схемой ролей), и провайдер никогда не пробует другой credential. Единственная проверка, которую провайдер делает сам при подключении, — тип развёртывания: сайт, который отвечает deploymentType не тем продуктом, который объявил оператор, отклоняется с подсказкой, какое значение поставить, потому что Cloud и Server / Data Center — разные API, и молча считать их совместимыми спецификация запрещает.
Запись в surface отсутствует: создание, правка, назначение, комментарии и переходы ждут pending actions и карточки подтверждения; удаление, администрирование проектов и workflow, произвольный REST и скачивание содержимого вложений — тоже. Провайдер читает только объявленный allow-list эндпоинтов /rest/api/3 (гейт пакета падает на любом пути вне него), а легаси-эндпоинт /rest/api/3/search, удалённый Atlassian, не используется: поиск идёт через /rest/api/3/search/jql.
Сервисный токен Jira
Подключение может работать от сервисного токена развёртывания: профиль называет сайт, секрет — обычный API-токен Atlassian в Basic-паре, а граница ресурсов — projects (ключи проектов, сравнение без учёта регистра; числовой id засчитывается, если Jira отдала его в выдаче). Сервисному токену недоступны чтения вложений — даже список метаданий помечен чувствительным, потому что следующий естественный шаг агента — скачать файл. Фильтры JQL по имени человека в сервисном режиме отказывают целиком — это чтение корпоративного справочника, у которого нет проекта в границе, — а фильтры по accountId и me остаются. Задача, названная по ключу, сначала раскрывается в свой проект, и чтение с уровнем безопасности, который сервисный токен не проходит, отвечает ResourceNotFound, как для чужой задачи.
Адрес TeamCity задаёт оператор: сервер один на весь стенд, поэтому teamcity.serverUrl — обычная настройка развёртывания, а не поле формы. Пользователь вводит только токен, и токен тратится ровно по этому адресу. Рядом оператор задаёт политику адресов: какие адреса этот стенд готов набирать вообще (политика осталась от времён, когда адрес вводил пользователь, и закрывает переезд конфига на чужой хост).
teamcity:
enabled: true
serverUrl: https://teamcity.example.internal
network:
mode: allowlist # allowlist | trusted-private
allowedHosts:
- teamcity.example.internal
- "*.corp.example"
allowedCidrs:
- 10.20.0.0/16 # только для адресов, записанных цифрами
allowedPorts:
- 443
- 8111
allowHttp: falseПравила простые и проверяются дважды — при сохранении подключения и на каждом вызове инструмента, поэтому ужесточение политики закрывает и ранее сохранённые подключения:
- только
http/https, HTTPS обязателен вне явногоallowHttp; в URL не может быть credentials, query и fragment; - имя хоста должно попасть в
allowedHosts(точное имя или*.суффикс), а адрес, записанный цифрами, — вallowedCidrs; - порт должен быть в
allowedPorts: по умолчанию разрешён только порт схемы, поэтому TeamCity на своём стандартном:8111требует явногоallowedPorts: [8111]; - редиректы не отслеживаются (
redirect: "error"), токен уходит только в заголовкеAuthorization: Bearer, в URL и теле его нет; mode: trusted-private— для доверенной корпоративной сети: имена хостов разрешены любые, а закрытые диапазоны (10/8,172.16/12,192.168/16,127/8) подставляются какallowedCidrsпо умолчанию. Он предполагает, что стенд доверяет своему DNS; если это не так, нуженallowlist.
Пустая политика (allowedHosts и allowedCidrs пусты) — это не ошибка загрузки, а «ничего не подключить»: плагин пишет об этом предупреждение в лог при старте, а карточка отвечает пользователю отказом. Опечатки в самой политике — наоборот, падение на старте: молча выброшенный шаблон хоста оставил бы стенд без объяснения.
Если для этого сервера заведён сервисный токен, форма вместо поля с токеном показывает чекбокс «Использовать сервисный токен»: при defaultForNewConnections: true он отмечен, кнопка называется «Подключить сервисный токен», а адрес сервера остаётся той же спра
