Документация Написание плагинов

Написание плагинов

Плагины расширяют Mockarty новым контентом, логикой и интерфейсом — пак готовых
моков, свой генератор $.fake.*, панель в сайдбаре. Этот гайд проведёт от пустой
папки до установленного рабочего плагина за несколько минут.

Если вам нужно только установить чужой плагин — смотрите
Плагины. Эта страница — для авторов.

Быстрый старт — создать, упаковать и включить плагин

Для комплекта моков CLI создаёт исходные файлы и упаковывает их. Работающий
экземпляр Mockarty и токен администратора нужны для установки и включения
готового файла.

mockarty-cli plugin create my-plugin        # scaffold (--template для других типов)
mockarty-cli plugin pack my-plugin           # → my-plugin-0.1.0.zip
mockarty-cli plugin install my-plugin-0.1.0.zip && mockarty-cli plugin enable my-plugin

Так вы включите стартовый комплект моков. Перед публикацией измените его
содержимое. Для WASM-фейкера дополнительно запустите ./build.sh
(нужен TinyGo). При итерациях
mockarty-cli plugin dev my-plugin --enable переустанавливает плагин после
каждого сохранения. Для другого типа плагина укажите --template при создании.

Что такое плагин

Плагин — это небольшой .zip-бандл с одним ключевым файлом plugin.json
(манифест) плюс ассеты (WebAssembly-модуль, HTML-панель). Манифест
декларирует, что плагин добавляет; Mockarty сам это рисует и применяет.
Механизмы:

Механизм Что вы пишете Где исполняется
Mock-kit только JSON — (декларативный контент)
WASM-фейкер крошечную функцию, скомпилированную в WebAssembly в изолированной песочнице
WASM-трансформер ответа крошечную функцию, скомпилированную в WebAssembly в изолированной песочнице, inline на пути ответа
WASM-матчер запроса крошечную функцию, скомпилированную в WebAssembly в изолированной песочнице, на пути матчинга
Кодек протокола кодировщик/декодировщик строковых фреймов в WebAssembly в песочнице за listener’ом, которым владеет хост
UI-панель (sidebar / page-slot / command / entity-tab / settings) HTML-страницу в sandboxed iframe
Вики-макрос JSON + text/template раскрывается на вики-страницах, виден в /-меню редактора
Коннектор только JSON переиспользует встроенный адаптер интеграции
Контент-пак только JSON наполняет другой модуль (wiki / дашборды / задачи / коллекции / тест-кейсы / контракты) по требованию

Плюс опциональные настройки (форма из JSON-Schema) на любом из них.
Бандл можно загрузить из файла без подключения к реестру.

Цикл авторинга

Всё ниже — через mockarty-cli. Scaffold, упаковка и dev-релоад работают
полностью офлайн; только install/enable требуют запущенного сервера и
админ-токена.

mockarty-cli plugin create my-plugin                 # scaffold (шаблон mock-kit)
mockarty-cli plugin pack my-plugin                   # → my-plugin-0.1.0.zip
mockarty-cli plugin install my-plugin-0.1.0.zip      # загрузка на сервер (админ)
mockarty-cli plugin enable my-plugin                 # включить

Выбор шаблона

create создаёт пакет, который уже проходит валидацию и проверку соответствия,
поэтому первым делом вы правите содержимое, а не боретесь со схемой:

--template Что создаётся
mock-kit (по умолчанию) набор готовых моков
wasm-faker генератор $.fake.* и build.sh (нужен TinyGo)
protocol-codec client-first строковый протокол, WASM-кодек и build.sh
ui-panel панель в боковом меню — HTML-страница в изолированном iframe
connector тип подключения поверх встроенного адаптера интеграций
content-pack стартовое содержимое для другого модуля (вики, дашборды, задачи, …)
link-type тип связи между сущностями с шаблоном URL для перехода
event-type событие, на которое могут подписаться другие модули
event-listener подписка на события хоста и собственное событие
task-type тип задания, которое умеет выполнять раннер
wiki-macro макрос, раскрывающийся на страницах вики
agent персона-специалист, которой агентская сеть может поручить работу
skill многоразовый рецепт, который хост выдаёт агенту по требованию
mcp-tool ваше задание раннера как полноценный инструмент MCP для агента
workflow-component ваше задание раннера как шаг миссии
aqc-surface своя поверхность качества и обработчик её свидетельства

Шаблон есть для каждого семейства вкладов, которое понимает сервер, кроме
одного: worker поставляет настоящие исполняемые файлы, и манифест
закрепляет sha256 каждого артефакта — выдумать двоичный файл каркас не может.
Начните worker с шаблона task-type и допишите блок workers руками.

Проверьте пакет раньше всех остальных

plugin test выполняет всё, что сервер проверил бы при установке, и вдобавок
то, что остаётся неверным даже после успешной установки: вклад без названия,
который никто не найдёт в списке; разрешение, запрошенное в манифесте, но
никому не нужное; поле настроек с именем api_token, которое хранило бы токен
открытым текстом. Ни сервер, ни сеть не нужны:

mockarty-cli plugin test ./my-plugin                 # каталог или собранный .zip
mockarty-cli plugin test ./my-plugin --strict        # считать ошибкой и предупреждения (для CI)
mockarty-cli plugin test ./my-plugin --report r.json # переносимый отчёт для ревьюера
mockarty-cli plugin test ./my-plugin --report r.json --sign-key key.b64  # с подписью

Каждая находка сопровождается способом исправления на следующей строке. Ошибки
завершают команду с ненулевым кодом, поэтому задание CI с этой командой не
опубликует пакет, который сервер откажется принимать.

Переменная MOCKARTY_CONFORMANCE_HOST_VERSION дополнительно сверяет объявленный
в манифесте диапазон версий с конкретной версией сервера; без неё эта проверка
пропускается, а не угадывается.

Автодополнение в редакторе и проверка в CI

plugin schema печатает JSON Schema манифеста — она генерируется из тех же
словарей, которые проверяет сервер, поэтому разойтись с ними не может:

mockarty-cli plugin schema --out plugin.schema.json

Укажите её редактору ("$schema": "./plugin.schema.json" в plugin.json) — и
получите автодополнение и подсветку ошибок прямо во время набора.

Пока итерируете — не гоняйте pack/install вручную, пусть dev следит и
переустанавливает:

mockarty-cli plugin dev my-plugin --enable           # переустановка на каждое сохранение

Проверить бандл можно в любой момент, без сервера:

mockarty-cli plugin inspect my-plugin-0.1.0.zip

Манифест

{
  "id": "acme.ru-fakers",
  "name": "Russian fakers",
  "version": "1.0.0",
  "description": "Что добавляет плагин.",
  "author": { "name": "Acme", "url": "https://acme.example" },
  "license": "MIT",
  "min_mockarty_version": "2.0.0",
  "contributes": { }
}
  • id — строчный slug с точками-неймспейсами (acme.ru-fakers). Это
    постоянная идентичность плагина; выбирается один раз.
  • version — semver. Установка старшей версии поверх младшей — апгрейд
    на месте с сохранением состояния «включён».
  • min_mockarty_version — опционально; не даёт установиться на старый сервер.
  • contributes — полезная нагрузка. Один или несколько разделов ниже.

Валидация строгая, а ошибки называют точное поле — опечатка падает на
pack/install, никогда молча.

Mock-kit (без кода)

Простейший плагин: готовый контур моков. После включения kit появляется в
каталоге mock-kit’ов и готов к развёртыванию в любой namespace.

{
  "contributes": {
    "mock_kits": [
      {
        "key": "demo_users_api",
        "name": "Users API (demo)",
        "description": "Стартовый контур /users.",
        "mocks": [
          { "route": "/users", "method": "GET", "status_code": 200,
            "body": { "users": [ { "id": "$.fake.UUID", "name": "$.fake.Name" } ] } },
          { "route": "/users", "method": "POST", "status_code": 201,
            "body": { "id": "$.fake.UUID", "created": true } }
        ]
      }
    ]
  }
}

Тела поддерживают те же динамические хелперы, что и любой мок ($.fake.*,
JSONPath). Scaffold: mockarty-cli plugin create my-kit --template mock-kit.

Мок кита умеет всё то же, что мок, сделанный руками

Кит имеет смысл выкладывать, только если контур получается полным — иначе тот,
кто его развернёт, будет достраивать руками самую интересную половину. Поэтому
мок кита принимает те же поля, что и обычный мок, с теми же именами:

Поле Что даёт
conditions Несколько моков на одном маршруте, различаемых телом запроса. Так кит выкладывает на одном эндпоинте и обычный ответ, и ошибку, и стриминг.
header_conditions, query_conditions То же, но по заголовкам или параметрам запроса. Условия по query — только для HTTP: вызов инструмента несёт аргументы, а не строку запроса.
headers Заголовки ответа — Retry-After при 429, свой трассировочный идентификатор. Если объявили хоть один заголовок, вы владеете всеми: application/json по умолчанию уже не добавляется.
delay Миллисекунды до ответа (до 120000). Медленный апстрим; работает и для HTTP, и для инструментов MCP.
priority Выбирает из моков со сработавшими условиями: побеждает большее значение. Мок со сработавшими условиями всё равно выше общего мока без условий.
llm Ответ в формате конкретного LLM-провайдера — см. ниже.
sse Явная цепочка server-sent events, для не-LLM потоков. Мок отдаётся клиентам event-stream по своему маршруту.
protocol + mcp Отдавать мок как инструмент MCP, а не как URL.

Среди моков со сработавшими условиями побеждает самый высокий приоритет; при равном
приоритете выбирается более точное совпадение. Мок без условий — это общий случай: он используется,
только если не подошёл ни один мок с условиями. Пишите общий случай без условий, каждый особый — с условиями, а
пересекающиеся особые случаи упорядочивайте приоритетом.

{
  "mocks": [
    { "route": "/v1/chat/completions", "method": "POST", "status_code": 429,
      "priority": 90,
      "conditions": [ { "path": "$.model", "assertAction": "equals", "value": "mock/rate-limit" } ],
      "headers": { "Content-Type": ["application/json"], "Retry-After": ["2"] },
      "body": { "error": { "message": "Rate limit reached.", "code": "rate_limit_exceeded" } } },

    { "route": "/v1/chat/completions", "method": "POST", "status_code": 200,
      "llm": { "provider": "openai", "model": "gpt-4o-mini",
               "content": "Вы сказали: $.req.lastUserMessage",
               "finishReason": "stop", "chunkChars": 6, "chunkDelayMs": 25 } }
  ]
}

Ответы LLM (llm)

provider задаёт формат на проводе: openai (подходит и для DeepSeek, Qwen,
Mistral, Ollama и прочих OpenAI-совместимых API) или anthropic. Для стриминга
второй мок не нужен
: если запрос содержит "stream": true, тот же самый мок
отдаётся потоком по токенам, а темп задают chunkChars и chunkDelayMs.
toolCalls возвращает вызов функции вместо текста, а finishReason должен
использовать словарь самого провайдера (stop/tool_calls/length у OpenAI,
end_turn/tool_use/max_tokens у Anthropic) — клиенты сравнивают строку
буквально.

Инструменты MCP (protocol: "mcp")

Поставьте protocol в "mcp", и мок будет отвечать на вызов инструмента, а не на
URL. mcp.tool — имя, которое видит клиент, mcp.description — то, что читает
модель, решая, применять ли инструмент, а mcp.input_schema — JSON Schema его
аргументов
; объявляйте её. Без неё сервер сможет только вывести схему из ваших
условий, а выведенная схема лишена required и срезает вложенные аргументы —
то есть ровно того, что нужно модели для корректного вызова. Признак
mcp_is_error: true возвращает ошибку инструмента.

{
  "protocol": "mcp",
  "mcp": {
    "tool": "browser_navigate",
    "description": "Перейти по URL и вернуть заголовок страницы.",
    "input_schema": {
      "type": "object",
      "properties": { "url": { "type": "string", "description": "Абсолютный URL" } },
      "required": ["url"]
    }
  },
  "body": { "content": [ { "type": "text", "text": "Переход на $.req.url" } ] }
}

Клиенты обращаются к мокнутому MCP-серверу по POST /stubs/<пространство> и
находят инструменты через tools/list. Готовые примеры обоих видов лежат в
examples/plugins/ — llm-openai, llm-anthropic и mcp-playwright,
mcp-github и другие.

WASM-фейкер (свой $.fake.*)

Фейкер-плагин добавляет новые генераторы динамических ответов — например валидные
национальные идентификаторы. Логика исполняется как WebAssembly-модуль в
запертой песочнице
: читать и писать он может только свою память — ни диска, ни
сети, ни часов. В этом вся модель безопасности: код-плагин не может тронуть
ничего без выданного гранта.

Scaffold:

mockarty-cli plugin create my-fakers --template wasm-faker

Получите манифест, исходник TinyGo и build-скрипт:

{
  "contributes": {
    "wasm": [
      { "point": "faker-provider", "module": "faker.wasm",
        "fn": "faker", "exports": ["my_id"] }
    ]
  }
}
  • module — скомпилированный .wasm-ассет в бандле.
  • fn — экспортируемая функция, которую вызывает хост.
  • exports — имена фейкеров, которые обслуживает функция. Указав my_id, вы
    делаете $.fake.my_id доступным в любом теле мока.

Контракт гостя

Mockarty передаёт запрошенное имя фейкера и ждёт значение обратно — маленькими
JSON-строками. Модуль экспортирует три вещи:

memory                          ваша линейная память
mk_alloc(size i32) -> i32       bump-аллокатор, возвращает указатель
<fn>(ptr i32, len i32) -> i64   пакует (outPtr << 32 | outLen) результата

Для точки faker вход — {"name":"<фейкер>"}, выход — {"value":"<строка>"}.
Одна функция обслуживает все имена из exports; ветвитесь по полю name.
Scaffold реализует всё это — вы правите только возвращаемое значение.

Сборка

Продукту для запуска плагина тулчейн не нужен — в нём собственный
WebAssembly-рантайм. TinyGo нужен только чтобы
скомпилировать модуль:

cd my-fakers && ./build.sh        # tinygo build -target=wasm-unknown -o faker.wasm .
mockarty-cli plugin pack .

Полноценный пример с валидными контрольными суммами (ИНН/СНИЛС/ОГРН/КПП) есть в
examples/plugins/ru-fakers в поставке продукта — хорошая отправная точка для
настоящих генераторов.

Случайность: у песочницы нет источника энтропии, поэтому засейте небольшой
PRNG, продвигающийся на каждый вызов. Тогда значения меняются от вызова к
вызову (никогда не кэшируются), но детерминированы в пределах запуска сервера —
идеально для воспроизводимых тестовых данных.

WASM-трансформер ответа (переписать тело мока)

Плагин-трансформер пост-обрабатывает отрендеренное тело мока прямо перед
отправкой — маскирует поле, оборачивает в конверт, вырезает токен. Исполняется как
WebAssembly-модуль в той же изолированной песочнице, что и фейкер, inline на
пути ответа, и только для тех моков, которые вы включили тегом.

{
  "contributes": {
    "wasm": [
      { "point": "response-transformer", "module": "transform.wasm",
        "fn": "transform", "exports": ["envelope"] }
    ]
  }
}
  • point — response-transformer.
  • fn — экспортируемая функция, которую хост вызывает с байтами тела.
  • exports — ключи трансформера, которые обслуживает эта функция. Указав
    envelope, вы разрешаете моку подключиться тегом transform:envelope.

Включение на уровне мока. Добавьте моку тег transform:<key>. Его
отрендеренное тело пропускается через трансформер, привязанный к <key>; моки без
тега не платят ничего. Несколько тегов transform: применяются по порядку.

Контракт гостя

Те же три экспорта, что у фейкера, но полезная нагрузка — сырое тело, не JSON:

memory                          ваша линейная память
mk_alloc(size i32) -> i32       bump-аллокатор, возвращающий указатель
transform(ptr i32, len i32) -> i64   пакует (outPtr << 32 | outLen) нового тела

transform получает байты тела в (ptr,len) и возвращает изменённое тело. Хост
ограничивает передаваемый вход (по умолчанию 1 МиБ) — тело больше отдаётся без
изменений, а не обрезается, поэтому рассчитывайте арену входа под этот лимит.
Трансформер, который ошибся или упал, пропускается, и отдаётся исходное тело — он
никогда не сломает ответ.

Рабочий пример, оборачивающий любое тело в
{"data":<body>,"transformedBy":"plugin"}, есть в
examples/plugins/response-transformer.

WASM-матчер запроса (кастомный оператор условия)

Плагин-матчер добавляет новый оператор условия — проверку, которой нет среди
встроенных (equals, contains, matches, gt/lt, one_of, …): например контрольная
сумма Луна или проверка контрольного разряда национального идентификатора.
Исполняется как WebAssembly-модуль в той же изолированной песочнице и только
для условий, которые его называют — каждый встроенный оператор решается без вызова
вашего модуля.

{
  "contributes": {
    "wasm": [
      { "point": "condition-matcher", "module": "match.wasm",
        "fn": "match", "exports": ["luhn", "divisible_by"] }
    ]
  }
}
  • point — condition-matcher.
  • exports — ключи операторов, которые обслуживает функция. Указав luhn, вы
    разрешаете условию использовать assertAction: "plugin:luhn".

Применение на моке. Задайте условию assertAction = plugin:<key>. Его
path выбирает проверяемое значение, value — операнд (для операторов, которым
он нужен, как divisible_by). Условие, чей матчер не установлен или выключен для
пространства, не сработает. Такой запрос не получит ответ от этого мока.

Контракт гостя

Те же три экспорта, что у фейкера. Хост передаёт вызванный ключ плюс значение и
операнд как JSON и ждёт булево:

memory                          ваша линейная память
mk_alloc(size i32) -> i32       bump-аллокатор, возвращающий указатель
match(ptr i32, len i32) -> i64  пакует (outPtr << 32 | outLen) результата

Вход — {"key":"<key>","actual":"<значение>","expected":"<операнд>"}; выход —
{"match":true|false}. Одна функция обслуживает все ключи из exports;
ветвитесь по полю key. Матчер, который ошибся или упал, считается несовпадением —
он никогда не сломает матчинг запроса.

Рабочий пример (plugin:luhn, plugin:divisible_by) есть в
examples/plugins/condition-matcher.

Кодек протокола (client-first TCP-строки)

Протокольный плагин добавляет безопасный кодек, а не сетевой сервер. Mockarty
владеет listener’ом, маршрутизацией namespace, лимитом фрейма 1 МиБ, разрешением
Socket-мока, журналом запросов, захватом неопределённых запросов и завершением
соединений. Гость только переводит фрейм до перевода строки в JSON-сообщение и
ответ выбранного мока обратно в один фрейм.

mockarty-cli plugin create my-codec --template protocol-codec
./my-codec/build.sh                    # нужен TinyGo
mockarty-cli plugin test ./my-codec

Манифест связывает один дескриптор ровно с одним WASM-экспортом с тем же ключом:

{
  "contributes": {
    "protocols": [{
      "key": "acme_line", "name": "ACME line", "transport": "tcp-line",
      "magic": "ACME "
    }],
    "wasm": [{
      "point": "protocol-codec", "module": "codec.wasm",
      "fn": "codec", "exports": ["acme_line"]
    }]
  }
}

Хост вызывает codec с
{"operation":"decode|encode","protocol":"acme_line","frameBase64":"..."}.
Для decode верните {"message":{...}}, для encode —
{"frameBase64":"..."}. Хост перезаписывает serverName и
pluginProtocol, поэтому гостевой код не может направить запрос в чужой протокол
или стереть атрибуцию. Дедлайн вызова — 100 мс; доступа к диску, сети,
переменным окружения и часам нет.

Запустите Mockarty с MOCKARTY_UNIFIED_PORT=1, включите плагин и выполните
mockarty-cli plugin protocols. Создайте Socket-мок с возвращённым
serverName и событием из decode; в разделе Socket Конструктора этот routing
заполняется из списка активных плагинов. Успешные вызовы попадают в обычный
журнал запросов, а несовпавшие декодированные сообщения — в Undefined Requests,
откуда их можно превратить в мок.

Первый транспорт намеренно ограничен: client-first, печатный ASCII magic длиной
2–16 байт, JSON-сообщение, фрейминг по переводу строки, поток request/reply.
Бинарный фрейминг, server-first handshake и произвольное состояние сокета
требуют нового транспорта хоста; прятать их в гостевом WASM нельзя.

UI-панель (страница в сайдбаре)

UI-плагин добавляет пункт сайдбара, открывающий вашу HTML-страницу. Страница
рендерится в sandboxed iframe из вашего бандла: строгий CSP, без same-origin,
поэтому она никогда не прочитает сессию или страницу хоста. С Mockarty она
общается только через postMessage.

mockarty-cli plugin create my-panel --template ui-panel
{
  "contributes": {
    "ui": [
      { "point": "sidebar", "id": "my_panel", "title": "My panel",
        "icon": "wrench-screwdriver", "panel": "panel.html" }
    ]
  }
}
  • icon — имя heroicon (без префикса hi-). Возьмите существующее, иначе
    слот отрисуется пустым.
  • panel — HTML-файл относительно корня бандла.

Mockarty добавляет текущую тему как ?theme=dark|light, чтобы панель совпадала
со светлой и тёмной. Scaffold panel.html уже учитывает тему и самодостаточен —
превратите его в свой интерфейс. Он следует за переключением темы вживую.

Встраивание панели inline (page-slot)

Кроме сайдбара, ui-вклад может отрисовать панель inline на странице хоста —
point: "page-slot" с target, называющим слот хоста:

{ "point": "page-slot", "id": "ops_slot", "title": "Ops", "target": "dashboard", "panel": "panel.html" }

Слоты есть на 12 экранах, включая dashboard, mocks, tasks (задачи),
test-cases (TCM), api-tester, security, chaos, contract-testing,
wiki; панель появляется там в том же sandboxed iframe.

Помимо сайдбара и page-slot доступны ещё четыре UI-точки, все на том же
sandboxed-iframe механизме:

  • command — действие в палитре Ctrl-K (категория «Плагины»), открывает
    панель плагина с любого экрана;
  • entity-tab — вкладка в карточке сущности: target = тип сущности
    (issue — рядом с «Комментарии/История», mock — рядом с
    «Конфигурация/Логи/Версии/Инфо», case — в билдере тест-кейса рядом с
    «Шаги…История»). Плагинные вкладки помечены маленьким значком-пазлом, чтобы
    отличаться от встроенных;
  • settings-section — sandboxed-панель плагина внутри модалки
    Настроить, под формой, которую хост генерирует из settings_schema
    (JSON Schema);
  • board-import — свой формат диаграмм в пикере импорта на досках
    (описан ниже).

Плагин может дать несколько точек из одного panel.html
(см. examples/plugins/ops-panel).

Свой формат диаграмм на досках (board-import)

Вклад board-import добавляет ваш формат в список импорта на доске — рядом с
Mermaid, draw.io и сценой доски, с бейджем Плагин. target — слаг формата:

{ "point": "board-import", "id": "plantuml", "title": "PlantUML",
  "target": "plantuml", "icon": "document-text", "panel": "import.html" }

Когда пользователь выбирает ваш пункт, хост загружает panel.html в обычной
песочнице — невидимо, в роли конвертера — и присылает туда введённые данные:

window.addEventListener('message', (e) => {
  if (e.data && e.data.type === 'mockarty:board-import-request') {
    const mermaid = convert(e.data.text);            // ваша конвертация
    parent.postMessage({ type: 'mockarty:board-import',
                         format: 'mermaid', payload: mermaid }, '*');
  }
});

Отвечайте format = mermaid, drawio или scene и payload — текстом в
этом формате; в фигуры его превратит сам хост. Чтобы сообщить о проблеме,
ответьте { type: 'mockarty:board-import', error: 'причина' } — пользователь
увидит её текстом. Панель, не ответившая за минуту, снимается.

Внешняя панель (panel_url) — подключите свой сервис

Вместо файла panel из бандла, ui-вклад может указывать на страницу вашего
собственного сервиса — задайте panel_url абсолютным https-URL (требуется
ровно одно из panel / panel_url, не оба сразу):

{ "point": "sidebar", "id": "status_board", "title": "Статус-борд",
  "panel_url": "https://plugin.example.com/board" }

Страница грузится в том же sandbox-iframe, что и панель из бандла: получает
подсказку ?theme=, но не имеет ни сессии хоста, ни same-origin доступа.
Используйте это, когда панель — живой продукт со своим бэкендом (дашборды,
SaaS-консоли): ничего не нужно пересобирать при каждом релизе, URL всегда
отдаёт вашу свежую версию. Пользователь видит origin панели в манифесте до
установки.

Вики-макрос (в стиле Confluence, без кода)

Макрос-плагин добавляет новый блок !plugin-<имя>:… на все вики-страницы и в
/-меню редактора — жест макросов Confluence, декларативно:

"contributes": { "wiki_macros": [
  { "name": "plugin-status", "title": "Статус-плашка", "icon": "tag",
    "params": [ { "name": "colour", "title": "Цвет", "default": "grey" } ],
    "template": "<span data-macro=\"plugin-status\" data-macro-args=\"{{.colour}}\">{{.text}}</span>" } ] }

Использование на странице: !plugin-status:colour=green SHIPPED. Аргументы —
пары key=value (значения с пробелами — в кавычках: key="a b"); остальные
слова приходят как {{.text}}. Префикс plugin- в имени обязателен — макрос
плагина никогда не затенит встроенный.

  • template — Go text/template. Его вывод снова проходит обычный
    markdown-конвейер — рендерер Markdown, затем санитайзер — поэтому макрос не может
    произвести ничего, что автор не набрал бы руками; скрипты и inline-стили
    вырезаются. Вывод ограничен 64 KiB. Непарсящийся шаблон падает при
    УСТАНОВКЕ, а не при просмотре страницы.
  • Закреплённая пара атрибутов data-macro / data-macro-args (на
    div/span) пропускается санитайзером, чтобы хост мог стилизовать вывод —
    статус-плашки получают цвета из коробки (grey/green/red/yellow/blue по
    первому токену args).
  • Действует включение per-namespace: там, где плагин выключен, строка макроса
    остаётся авторским текстом — видимая деградация, никогда тихая потеря.
  • Включённые макросы автоматически появляются в /-меню редактора с меткой 🧩.

Рабочий пример: examples/plugins/wiki-status-macros (статус-плашка и панель
DECISION).

Коннектор (плагин для внешнего инструмента)

Коннектор-плагин расширяет Mockarty к внешнему инструменту — Jira, GitHub,
GitLab, TestRail — без кода. Он поставляет предконфигурированную привязку к
адаптеру интеграции, который у Mockarty уже есть, поэтому установка сразу даёт
готовый коннектор с разумными дефолтами.

mockarty-cli plugin create my-jira --template connector
{
  "contributes": {
    "connectors": [
      { "key": "jira_cloud", "name": "Jira Cloud", "kind": "jira",
        "icon": "bug-ant",
        "config_template": { "base_url": "https://your-org.atlassian.net", "project_key": "QA" } }
    ]
  }
}
  • kind — встроенный адаптер: jira, github, gitlab, testrail и другие.
    Проверяется при установке; неизвестный kind отклоняется — плагин
    переиспользует готовый адаптер, а не поставляет свой.
  • config_template — дефолты, которыми предзаполняется интеграция при её
    создании из коннектора. Никаких секретов здесь — учётные данные вводятся
    при настройке.

Включённые коннекторы отдаются в GET /api/v1/plugin-connectors и в UI
«Плагины»; создание интеграции из коннектора использует его kind и
config_template как дефолты, а вызовы идут через встроенный адаптер. Это
паттерн для любого трекера/CI/чата: переиспользуй готовый адаптер, оформи
коннектор, распространяй как плагин. Пример examples/plugins/jira-connector —
полная рабочая отправная точка.

Настройки (форма конфигурации)

Плагин может объявить схему настроек — JSON Schema своих конфигурируемых
опций. При её наличии страница «Плагины» показывает форму Настроить,
сгенерированную из схемы, а сохраняемые значения валидируются против неё.

{
  "id": "acme.ops",
  "name": "Ops",
  "version": "1.0.0",
  "settings_schema": {
    "type": "object",
    "properties": {
      "heading": { "type": "string", "title": "Заголовок панели" },
      "refresh_seconds": { "type": "integer", "minimum": 1, "maximum": 60 },
      "show_clock": { "type": "boolean", "title": "Показывать часы" }
    }
  },
  "contributes": { "ui": [
    { "point": "sidebar", "id": "ops", "title": "Ops", "panel": "panel.html" },
    { "point": "settings-section", "id": "ops_settings", "title": "Превью Ops",
      "icon": "wrench-screwdriver", "panel": "panel.html" }
  ] }
}

Форма поддерживает string, number/integer (с minimum/maximum), boolean и
enum (выпадающий список), плюс title и description. Админская форма
сохраняет дефолт плагина для инстанса; если у пространства есть
переопределение, в нём действует оно. Оба слоя сохраняются при апгрейде.

Для settings-section обязательны settings_schema и ровно один из
panel / HTTPS panel_url; target не нужен. Секция монтируется только пока
плагин активен в текущем пространстве, помечается видимым бейджем Плагин и
работает в той же строгой песочнице, что остальные plugin-панели. Это
дополнительный интерфейс: значения по-прежнему сохраняет форма хоста.

Не храните секреты в настройках. Настройки читаемы любым пользователем,
который может открыть плагин (хост передаёт их панели), поэтому считайте их
публичной конфигурацией — URL, метки, переключатели. Учётные данные — в
secret store, не здесь.

Чтение настроек в панели. Sandboxed-панель не может сама запросить свои
настройки (у неё нет аутентифицированного origin), поэтому хост читает их и
присылает через postMessage при загрузке панели. Bundled-панель получает
значения, действующие в текущем пространстве. Внешний panel_url обязан
объявить permission ui:external-panel; без этого проверенного разрешения
настройки внешнему origin не передаются. Подпишитесь:

window.addEventListener('message', function (ev) {
  if (ev.source !== window.parent) return; // доверяем только хосту
  if (!ev.data || ev.data.type !== 'mockarty:settings') return;
  var s = ev.data.settings || {};
  if (s.heading) document.querySelector('h1').textContent = s.heading;
});

Пример examples/plugins/ops-panel делает ровно это.

Подпись (опционально, для регулируемых сред)

Сервер может требовать, чтобы плагины были подписаны ключом, которому доверяют
его операторы. Сгенерируйте пару ключей, подпишите бандл и передайте публичный
ключ оператору сервера:

mockarty-cli plugin keygen --out mykey                # mykey (приватный), mykey.pub
mockarty-cli plugin pack my-plugin --sign mykey       # подписанный бандл

Оператор настраивает, каким ключам доверять (и обязательна ли подпись).
Неподписанный или недоверенный бандл на таком сервере будет отклонён.
mockarty-cli plugin inspect показывает статус подписи бандла.

Контент-пак — наполнить другой модуль (без кода)

Контент-пак шипит готовый контент для модуля за пределами мокинга и позволяет
юзеру инстанцировать его по требованию в namespace. Сейчас подключены модули Wiki (Confluence-style стартер), Dashboards (виджет-дашборд)
и Issue Tracker (Jira-style стартовый проект с issues); collections — той же формы. Ничего не создаётся, пока пак не инстанцируют, поэтому enable плагина не
трогает данные namespace.

"contributes": { "content_packs": [
  { "key": "team_wiki", "name": "Team wiki starter", "kind": "wiki",
    "description": "Runbook, ADR и PRD.",
    "items": [
      {"title": "Incident runbook", "content": "# Incident runbook\n…",
       "children": [{"title": "Postmortem", "content": "# Postmortem\n…"}]},
      {"title": "ADR", "content": "# ADR-000\n"}
    ] } ] }
  • kind — какой модуль наполняет пак: wiki (страницы, вложенность через children)
    или dashboard (виджет-дашборд: items = [{name, description, widgets:[{widgetType, dataSourceId, x, y, w, h, params?}]}]), или issue (стартовый проект + issues: items = [{name, keyPrefix, description?, issues:[{title, type, description?, priority?}]}], type ∈ task/story/bug/epic), или collection (коллекции API-Tester: items = [{name, protocol?, requests:[{name, method?, url, body?}]}]), или tcm (тест-кейсы с ручными шагами: items = [{name, priority?, expectedResult?, tags?, steps:[{action, expectedResult?}]}]), или contract (стартовые API-контракты, публикуемые в реестр: items = [{serviceName, specContent, specType?, version?, description?, tags?}] — тип спеки авто-определяется, если не указан), или board (шаблоны досок: items = [{name, description?, tags?, scene?}], где scene — обычный Excalidraw JSON; см. examples/plugins/board-templates). Паки, нацеленные на лицензируемый модуль (API Tester, TCM, контрактное тестирование, доски), требуют эту фичу на неймспейсе.
  • items — kind-специфичный контент (для wiki — дерево {title, content, children}).

После install + enable пак появляется в каталоге контент-паков
(GET /api/v1/content-packs, или MCP-тул content_pack_list) для namespace, где
плагин активен; инстанцируйте через POST /api/v1/content-packs/<key>/instantiate
(или content_pack_instantiate). Пример: examples/plugins/team-wiki-pack.

Типы связей — отношения сущностей с внешними системами (без кода)

Link-вклад объявляет типизированное отношение между сущностью Mockarty и
другой системой — «у этого issue есть Jira-тикет», «этот мок пришёл из GitHub
issue». URL-шаблон превращает сохранённый ключ в кликабельный deep link (каждый
{key} заменяется), так что связанная сущность — в одном клике от своего
двойника.

"contributes": { "links": [
  { "key": "jira_ticket", "name": "Jira ticket", "icon": "bug-ant",
    "from_entity": "issue", "to_entity": "external",
    "url_template": "https://your-org.atlassian.net/browse/{key}" } ] }
  • from_entity / to_entity — одно из mock, issue, case, wiki,
    collection, dashboard, contract, scan, external.
  • url_template — опц. абсолютный http(s) URL; {key} = сохранённое значение.

После включения типы видны в GET /api/v1/entity-link-types (фильтр
?entity=issue) для namespace, где плагин активен, а карточки сущностей
(напр. issue) показывают секцию Внешние связи для добавления/удаления
хранимых связей (POST/GET/DELETE /api/v1/entity-links; MCP entity_link_*).
Пример: examples/plugins/tracker-links.

Типы событий — расширить каталог нотификаций (без кода)

Event-вклад добавляет события, на которые пользователи подписываются
per-channel
ровно как на встроенные (email, Telegram, Slack, in-app …). Они
появляются в каталоге нотификаций в категории plugin.

"contributes": { "event_types": [
  { "type": "plugin.deploy.started",  "title": "Deploy started",  "severity": "info" },
  { "type": "plugin.deploy.failed",   "title": "Deploy failed",   "severity": "error",
    "description": "Деплой упал и требует внимания." } ] }
  • type — обязан быть namespaced plugin.<name> — никогда не затенит
    встроенное событие.
  • severity — info (дефолт), success, warning, error или critical.
  • default_subscribed — подписать пользователей по умолчанию (редко).

Пример: examples/plugins/deploy-events.

Отправка событий во время работы

Поверхность плагина с API-токеном — managed worker, внешний процесс, панель
через страницу хоста — публикует объявленное событие так:

curl -X POST "https://your-mockarty/api/v1/plugin-events/<plugin-id>/emit" \
  -H "Authorization: Bearer <api-token>" -H "Content-Type: application/json" \
  -d '{"eventType":"plugin.deploy.failed","correlationId":"deploy-42",
       "payload":{"stage":"migrate"}}'

Принимаются только типы, объявленные в манифесте, только пока плагин
установлен и активен в пространстве имён; payload — JSON-объект до 256 КиБ.
Хост сам проставляет версию конверта, id плагина и необязательную связку
correlationId/causationId — подписчики получают их под этими же ключами.

Типы задач — диспетчеризуемые джобы для внешних раннеров (без кода)

Task-вклад именует новый вид runner-джоба. В паре с внешним раннером,
объявляющим соответствующую capability (стандартный протокол раннеров),
отправленные задачи этого типа диспетчеризуются ему — так вендор поставляет
тяжёлую, внепроцессную функциональность.

"contributes": { "task_types": [
  { "type": "plugin-dbt-run", "name": "dbt run",
    "description": "Выполнить dbt-прогон на внешнем раннере." } ] }
  • type — строчный slug с namespace plugin-<name>, максимум 19 символов —
    никогда не затенит встроенный тип. Plugin-типы всегда licence-free —
    достижимость управляется enable-состоянием самого плагина.

Пример: examples/plugins/custom-runner-tasks.

Импорт из другого инструмента (WireMock / Postman / Mockoon / Bruno / Insomnia / Connect / n8n)

Уже есть библиотека моков в другом инструменте? Превратите её в Mockarty-плагин
одной командой — общая форма «маршрут + метод + статус + JSON-тело» конвертируется
прямо в mock-kit:

mockarty-cli plugin import ./mappings      --from wiremock --id acme.legacy-stubs
mockarty-cli plugin import ./collection.json --from postman  --id acme.api
mockarty-cli plugin import ./env.json        --from mockoon  --id acme.env
mockarty-cli plugin import ./my-collection   --from bruno    --id acme.api
mockarty-cli plugin import ./export.json     --from insomnia --id acme.api
  • WireMock — один файл-маппинг, файл {"mappings":[…]} или папка с
    .json-файлами по маппингу. Обрабатываются jsonBody/body и urlPath/url.
  • Postman — коллекция v2.1; вложенные папки обходятся, первый сохранённый
    пример-ответ каждого запроса становится моком. Переменные пути {{var}} → :var.
  • Mockoon — экспорт окружения; берётся default- (или первый) ответ маршрута.
  • Bruno — папка коллекции с .bru-файлами (передайте папку). Каждый
    запрос становится 200-моком; блок body:json — телом мока, {{var}} в пути
    превращается в :var. Bruno не хранит примеры ответов, так что ответы
    начинаются с тела запроса или пустыми — отредактируйте после импорта.
  • Insomnia — файл экспорта v4, JSON или YAML (Application → Export).
    Запросы становятся 200-моками (JSON-тела переносятся); префиксы
    {{ _.base_url }} отрезаются, шаблоны в пути становятся :params.

Команда пишет готовый к проверке каталог плагина; дальше pack и install как
обычно. Тело-JSON-объект проходит как есть; массив или скаляр верхнего уровня
оборачивается в {"response": …} (тело kit — объект).

Приложения Atlassian Connect (--from connect)

Connect-приложение — это уже «дескриптор + ваш hosted-сервис + iframe-страницы +
вебхуки», та же форма, что плагин Mockarty с внешними панелями. Дескриптор
конвертируется как есть:

mockarty-cli plugin import ./atlassian-connect.json --from connect
  • generalPages / adminPages / webSections становятся панелями в сайдбаре,
    webPanels — панелями page-slot на дашборде; все открывают ваш существующий
    https-сервис
    через panel_url (бэкенд менять не нужно; --id необязателен —
    выводится из Connect key).
  • webhooks превращаются в подписываемые типы событий плагина; scopes
    записываются как информационные permissions, видимые при установке.
  • Не переносится: JWT-хендшейк, workflow-модули и query-плейсхолдеры
    {context.param} (query отбрасывается из URL панелей) — панель работает в
    sandbox Mockarty, а не в контекст-протоколе Jira. baseUrl дескриптора должен
    быть реальным https-URL (шаблонные dev-значения вроде {{localBaseUrl}}
    отклоняются с понятной ошибкой).

Просмотрите сгенерированный plugin.json, затем pack и install как обычно.

Ноды сообщества n8n (--from n8n)

Библиотека нод n8n — огромный каталог описаний интеграций. Направьте конвертер
на папку нод (community-пакет или подкаталоги n8n-nodes-base/nodes) — и он
выдаст ОДИН connector-плагин, где каждая нода становится пресетом интеграции:

mockarty-cli plugin import ./nodes --from n8n
  • Ноды, совпадающие со встроенным адаптером Mockarty (github, gitlab,
    jira, jenkins, linear), переиспользуют его; остальные становятся
    пресетом webhook_generic, заполненным базовым API-URL ноды, если исходник
    его объявляет.
  • Имя сервиса, описание, категория и ссылка на документацию переносятся;
    trigger-варианты складываются в базовую ноду.
  • Логика исполнения нод и credential-флоу НЕ переносятся — пресет даёт
    идентичность сервиса и базовый API, чтобы интеграция создавалась в два
    клика; это не runtime воркфлоу.

--id необязателен (по умолчанию n8n.connectors). Просмотрите, pack,
install.

Публикация в реестр

Нет центрального магазина, который бы вас фильтровал. Реестр — это просто
Git-репозиторий с index.json, перечисляющим плагины и указывающим на их
скачиваемые zip’ы — та же модель, что Homebrew tap. Подготовьте запись:

mockarty-cli plugin publish my-plugin-1.0.0.zip \
  --download-url https://github.com/you/registry/releases/download/my-plugin-v1.0.0/my-plugin-1.0.0.zip

Добавьте напечатанный объект в index.json реестра, загрузите zip как
релиз-ассет — и плагин станет устанавливаемым по id на любом сервере,
направленном на этот реестр. Стартовый index.json и примеры-плагины
есть в папке examples/plugins/ в поставке продукта.

Сервер vs десктоп

Плагин ведёт себя одинаково на полном сервере и в десктоп-сборке — бандл хранится
и применяется одинаково. WASM-фейкеры и UI-панели работают в обоих. В кластере
включение плагина на одной ноде автоматически распространяется на остальные.
Ничего десктоп- или сервер-специфичного в плагине нет.

Чек-лист

  • plugin.json валиден (mockarty-cli plugin inspect)
  • Один понятный раздел contributes на каждую добавляемую вещь
  • WASM: .wasm собран и упакован; фейкеры перечислены в exports
  • UI: icon существует; панель учитывает тему; нет предположения о доступе к DOM хоста
  • Semver поднят на каждое изменение, которое вы выпускаете