Skip to content

Плагины ​

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

Плагины: Technitium DNS — зоны и записи своих доменов на своих серверах имён, делегирование, DNSSEC; Cloudflare DNS — то же для зон у Cloudflare; почта — ящики, алиасы и DKIM docker-mailserver, записи MX/SPF/DKIM через DNS-плагин; LiteLLM — виртуальные ключи к моделям с бюджетом и лимитами, модели, расход; MikroTik — роутеры как хосты плагинов (устройства без агента nkt в общем списке хостов); Yandex Cloud — ВМ, диски и снимки, группы безопасности, бакеты, Cloud DNS для хаба, базы, баланс.

Как это устроено ​

  • Плагин слушает HTTP на 127.0.0.1 своего хоста (в compose — 127.0.0.1:9180:9180): снаружи его не видно.
  • Хаб ходит к нему через SSH-соединение с хостом (плагин на машине хаба — напрямую). Хост, подключённый только туннелем (без SSH), для плагина не подходит.
  • Каждый вызов подписан общим секретом: NKT_PLUGIN_SECRET из .env конвейера плагина. Хаб хранит его зашифрованным, в экспорт он уходит вместе с остальными секретами хаба.
  • Плагин описывает себя манифестом: разделы, операции, команды сценариев, инструменты MCP, что он делает для хаба и какие права у хаба просит.

Подключить ​

  1. Выложите плагин конвейером (пример — в его справке). В .env конвейера — NKT_PLUGIN_SECRET: generate:hex32 и, если порт не 9180, NKT_PLUGIN_PORT.
  2. «О системе» → «Плагины» → «Подключить» → «С конвейера», выберите конвейер. Хост, порт и секрет хаб возьмёт из него сам.
  3. «Проверить» — хаб покажет, что плагин добавит: разделы, команды сценариев, инструменты MCP, что он делает для хаба и какие права просит. Совпадения (слово сценария или инструмент уже есть) — там же.
  4. «Подключить». В меню хаба появится раздел «Плагины».

«Вручную» — хост, порт и секрет без конвейера (плагин, установленный не через nkt). «Отключить» убирает разделы, команды и инструменты из хаба и отзывает токен плагина; сам плагин и его данные остаются на хосте.

Каталог плагинов ​

Плагины и примеры конвейеров не встроены в nkt: они живут в examples/ репозитория, а CI собирает из них каталог и выкладывает его рядом с этим сайтом — https://piqab.github.io/nkt/catalog/. Новый плагин или исправление в старом доходят до хаба без выпуска nkt.

«О системе» → «Плагины» → «Каталог»: версия каждого плагина, отметка «тесты на подделках пройдены», где он уже поставлен и есть ли новее, «Справка» (README и скриншоты плагина) и «Установить на хост».

Каталог плагинов

Справка плагина из каталога

  • Подпись. Каталог подписан ключом ed25519; открытый ключ встроен в nkt. Хаб проверяет подпись и SHA-256 каждого файла — при скачивании, при загрузке архива и при чтении своей копии с диска. Не сошлось — каталог не принимается, работает прежняя копия. Проверить подпись можно и без nkt: openssl pkeyutl -verify -pubin -inkey ключ.pub -rawin -in index.json -sigfile index.json.sig.
  • Тесты. CI прогоняет тест каждого плагина на подделках (deploy/test_plugin.py); упавший плагин в каталог не попадает, остальные выходят.
  • Копия на хабе — в DataDir/catalog, обновляется раз в 6 часов и кнопкой «Обновить каталог» (заданием). Установка, обновление плагинов и «Выкладки» → «Примеры» работают из копии — и без интернета.
  • Без интернета: скачайте https://piqab.github.io/nkt/catalog.tar.gz на машине с интернетом и загрузите кнопкой «Загрузить архив» — проверка та же.
  • Совместимость. У плагина есть min_nkt: хабу старше — «Установить» неактивна с пояснением.
  • Свой каталог («Источник каталога», с диффом и историей): форк репозитория со своим ключом — openssl genpkey -algorithm ed25519, закрытый ключ в секрет NKT_CATALOG_KEY форка (содержимое файла целиком, со строками BEGIN/END; склеенный в одну строку тоже читается), открытый (openssl pkey -pubout) — в поле «Свои открытые ключи»; или свой сервер с тем же устройством (index.json, index.json.sig, файлы). Свой ключ добавляется к встроенному. Настройки источника едут в экспорт хаба.

Автору плагина: папка examples/<имя>/ — deploy/nkt-plugin.json (с version, min_nkt, protocol), deploy/nkt-plugin.py (VERSION — как в JSON), deploy/test_plugin.py, README.md и README.en.md, скриншоты в screens/ru/ и screens/en/. Поменяли файлы плагина — поднимите version, иначе сборка каталога остановится: хабы иначе не узнали бы об обновлении. Пример конвейера — pipeline*.yaml и nkt-example.json (заготовка для «Примеры ▾»).

Свой плагин ​

Плагин, которого нет в каталоге, — например, написанный по руководству, — администратор загружает в хаб сам: «О системе» → «Плагины» → «Каталог» → «Загрузить свой плагин».

Загрузить свой плагин

  1. Архив папки плагина (.tar.gz или .zip) — как examples/<имя>/ в репозитории: deploy/nkt-plugin.json, deploy/nkt-plugin.py, README, screens/. Тест можно оставить — хаб его не хранит и не запускает.
  2. Проверка хабом — та же статическая, что nkt-catalog lint у каталога: пакет, версия = VERSION в коде, тексты ru/en, секретные настройки, запрещённый код (shell=True, os.system, eval, команда строкой). Ещё — пути в архиве (без .., ссылок и абсолютных путей), размеры (файл до 2 МБ, всё до 10 МБ), имя не из каталога, версия выше уже загруженной. На хабе плагин не запускается.
  3. Код целиком (а у новой версии — дифф с прежней). Отметьте «Код просмотрен» и «Сохранить»: плагин будет работать службой на хостах, и отвечает за него тот, кто загрузил. В журнале действий — кто, версия, SHA-256.
  4. В каталоге хаба плагин — с меткой «свой»: «Установить на хост», «Обновить на хосте» после загрузки новой версии — как у плагинов каталога; манифест хаб проверяет при подключении. «Удалить свой» убирает его из каталога хаба (если стоит на хостах — с подтверждением; установки остаются до удаления с хоста).

Загружает только администратор и только в интерфейсе (не API-токен и не MCP). Свои плагины и история последних 10 версий кода едут в экспорт хаба.

Пример по шагам — url-check (проверка адресов: код ответа, время, срок сертификата, находки, команда urlcheck АДРЕС):

  1. «Скачать пример» в окне загрузки (https://piqab.github.io/nkt/samples/url-check.tar.gz) или из репозитория: tar czf url-check.tar.gz -C examples/_samples url-check.
  2. «Загрузить свой плагин» → архив → код → «Код просмотрен» → «Сохранить».
  3. «Проверка адресов» (свой) → «Установить на хост» → в форме адреса через запятую → задание.
  4. Раздел плагина «Адреса» и «Находки»; в сценарии после выкладки — urlcheck https://shop.example.com/health.

Установить на хост (без Docker) ​

Плагин из каталога можно поставить службой прямо на хост, без контейнера: «О системе» → «Плагины» → «Установить на хост» → плагин, хост и его настройки (форма своя у каждого плагина) → задание в стандартном окне журнала.

Установить плагин на хост

  • Хаб кладёт файл плагина из проверенной копии каталога в /opt/nkt-plugins/<имя>/, настройки — в /etc/nkt-plugins/<имя>.env (права 0600, только root, без истории конфигураций), юнит nkt-plugin-<имя>.service: отдельный пользователь (DynamicUser), ProtectSystem=strict, слушает только 127.0.0.1. Если на хосте в /etc/nsswitch.conf нет модуля systemd (пакет libnss-systemd), временный пользователь systemd ни на какое имя не отображается, и ssh внутри плагина (Windows, connector) падает с «No user exists for uid …». Тогда хаб заводит постоянного системного пользователя nktp-<имя> без входа и домашнего каталога и удаляет его вместе с плагином. Уже поставленный плагин переходит на него по «Обновить на хосте».

  • Каталоги плагина в /opt/nkt-plugins агент создаёт и удаляет вне своей песочницы, как пишет конфигурации, — /opt для службы nkt только для чтения.

  • Секреты (токены из формы и секрет подписи, который создаёт сам хаб) не попадают в параметры задания: одноразовое хранилище в памяти хаба, 15 минут.

  • Нет python3 — задание поставит пакет.

  • Плагин подключается к хабу сам. «Обновить на хосте» — файл плагина версии хаба и перезапуск (настройки остаются); «Удалить с хоста» — служба, файлы и подключение.

  • «Настройки» — у плагинов без своих настроек (Windows, MikroTik, connector: учётки устройств хранит хаб) этой кнопки нет. Остальные — те же поля, что при установке (токен, адрес, зоны…), без переустановки. Обычные значения хаб читает с хоста; секреты не показываются никому: «задан» и поле «Заменить» (пусто — прежний). «Сверить» — дифф, «Сохранить» — заданием: файл настроек и перезапуск службы; плагин не ответил — прежние настройки возвращаются сами, в журнале задания — последние строки журнала службы. История обычных значений (20 версий, без секретов) — в хабе, версию можно вернуть. У DNS-плагина после сохранения хаб сразу сверяет свои записи. Новая версия плагина добавила обязательное поле — пометка «нужна настройка». У плагина, выложенного конвейером, настройки — .env конвейера.

    Настройки плагина

  • Нужен nkt на хосте этой версии или новее (вызов /api/plugins/native агента).

  • Несколько экземпляров одного плагина (два аккаунта Cloudflare, два Technitium) — разные имена (dns, dns2): по имени — служба nkt-plugin-<имя>, инструменты MCP и действие токена; порт на хосте хаб выбирает свободный. Слово сценария остаётся за первым, у второго — plugin dns2 dns ….

  • Хаб в Docker — на его машину плагин службой не поставить (в контейнере нет systemd): её нет в списке хостов окна установки.

  • Доступ к Docker ("docker": true в описании установки — например, плагину почтового сервера, которым управляют командой в контейнере): служба в группе docker, это права root на хосте — окно установки предупреждает заранее.

  • После обновления хаба у плагинов, чей файл в каталоге хаба новее, — пометка «есть версия новее» и кнопка «Обновить все» (одним заданием; сами не обновляются).

Где размещать плагины ​

МестоКогда
На хосте самой службыплагин управляет локальной службой (Technitium, почтовый сервер, Garage): её API на 127.0.0.1, наружу ничего не открывается
На машине хабаплагин ходит во внешний API (Cloudflare, Proxmox в сети хаба): проще всего; секреты внешней системы — на машине хаба
На отдельном хосте «интеграций»тоже внешние API, но секретам не место рядом с хабом или у хаба нет доступа в нужную сеть

Плагин недоступен — его разделы показывают ошибку, сверка DNS повторяется сама; остальной хаб работает.

Каталог — описание установки рядом с файлом плагина в репозитории (examples/<пример>/deploy/nkt-plugin.json):

json
{
  "name": "dns",
  "title": {"ru": "Technitium DNS", "en": "Technitium DNS"},
  "run": "python3",
  "file": "nkt-plugin.py",
  "port": 9180,
  "settings": [
    {"env": "DNS_API", "default": "http://127.0.0.1:5380/api", "title": {"ru": "…", "en": "…"}},
    {"env": "DNS_API_TOKEN", "secret": true, "required": true, "title": {"ru": "…", "en": "…"}}
  ]
}

Права ​

КтоЧто
Любой вошедший, токен чтенияразделы плагина и операции «только чтение»
Администраторвсё, что умеет плагин
Токен по правамоперации, которые меняют, — с действием «плагин ИМЯ» в окне токена
Сам плагинвызовы хаба — токеном по правам только с теми действиями, которые он просил (hub_actions) и которые вы подтвердили при подключении

Каждое действие плагина (операция, правка документа, команда сценария) — в журнале действий хаба: plugin.op, plugin.doc, plugin.command, подключение — plugin.connect.

Разделы ​

Раздел — то, что плагин показывает: таблицы, текст, кнопки. Кнопки вызывают операции плагина (с формой и подтверждением), открывают другой раздел («открыть зону») или правят документ — текст (например, файл зоны) в том же окне, что и всё остальное в nkt: дифф перед записью и история версий в хабе. Если документ изменился, пока вы его правили, запись не пройдёт — хаб покажет новый текст.

Сценарии ​

Команда плагина пишется так:

plugin ИМЯ СЛОВО аргументы…

или одним словом, если его не занял другой плагин (dns set …). «Проверить» спрашивает плагин, правильно ли написано; в сухом прогоне плагин только отвечает, что сделал бы. Справка по словам — в разделе плагина и в командах сценариев.

MCP ​

Операции, которые плагин отметил для агентов, — инструменты <имя>_<операция> (dns_zones, dns_record_set). Токен чтения видит только операции чтения, остальные — с действием «плагин ИМЯ». Отключили плагин — инструментов нет в tools/list.

Протокол (для своих плагинов) ​

Версия 1. Плагин — HTTP-сервер на любом языке.

Подпись. Каждый запрос хаба несёт заголовки:

  • X-NKT-Timestamp — время в секундах Unix;
  • X-NKT-Signature — hex HMAC-SHA256 строки <отметка>.<путь>.<тело> секретом (<путь> — /nkt/manifest или /nkt/call, тело GET — пустое);
  • X-NKT-Lang — ru или en: тексты плагин отдаёт на этом языке.

Отметка старше 5 минут или неверная подпись — ответ 401.

GET /nkt/manifest:

json
{
  "protocol": 1,
  "name": "dns",
  "title": "Technitium DNS",
  "version": "1.0.0",
  "description": "…",
  "ops": [
    {"name": "zones", "title": "Зоны", "read_only": true, "mcp": true,
     "description": "List zones", "input": {"type": "object", "properties": {}}},
    {"name": "record_set", "mcp": true, "destructive": false,
     "input": {"type": "object", "properties": {"name": {"type": "string"}}, "required": ["name"]}}
  ],
  "pages": [{"id": "zones", "title": "Зоны", "op": "zones"}],
  "commands": [{"word": "dns", "usage": "dns set ИМЯ ТИП ЗНАЧЕНИЕ [ttl СЕК]"}],
  "provides": ["dns"],
  "hub_actions": []
}
  • name — латиница, цифры, дефис (2–32); операции и разделы — латиница, цифры, _. Раздел ссылается на операцию «только чтение»; "hidden": true — не кнопкой вверху, а только переходом из другого раздела (open с аргументами).
  • commands — слова сценариев; слова языка (on, fly, deploy…) занять нельзя.
  • provides: ["dns"] — плагин ведёт DNS-записи для хаба (ниже).
  • hub_actions — действия токена по правам (pipeline.deploy, fly.manage…), которые плагин просит у хаба.

POST /nkt/call — {"op": "…", "args": {…}, "user": "admin", "dry_run": false} → {"ok": true, "text": "…", "result": …, "view": …} или {"ok": false, "error": "текст для человека"}.

Вид (view) раздела:

json
{"type": "table", "title": "Зоны", "note": "…",
 "columns": [{"key": "zone", "title": "Зона"}],
 "rows": [{"cells": {"zone": "example.com"},
           "actions": [{"title": "Записи", "open": "records", "args": {"zone": "example.com"}},
                       {"title": "Файл зоны", "args": {"zone": "example.com"},
                        "edit": {"get": "zone_text", "put": "zone_import", "doc": "example.com"}},
                       {"title": "Удалить", "op": "zone_delete", "args": {"zone": "example.com"},
                        "confirm": "Удалить зону example.com?", "danger": true}]}],
 "actions": [{"title": "Добавить зону", "op": "zone_add",
              "fields": [{"name": "zone", "title": "Зона", "required": true}]}]}

type — table, text (text) или sections (items — виды подряд). Поля формы: text, textarea, number, select (options), bool, password (скрытый ввод; значение не пишется в журнал действий). Документ: операция get отдаёт result.text, put получает args.text; историю версий ведёт хаб.

Кнопка с dns — записи, которые плагину нужны в DNS (почта: MX, SPF, DKIM): {"title": "…", "dns": {"owner": "domain:example.com", "records": [{"name": "example.com", "type": "MX", "values": ["10 mail.example.com."], "ttl": 3600}]}}. Хаб ставит их через подключённый DNS-плагин (владелец plugin:<имя>:<owner>, типы A, AAAA, CNAME, TXT, MX) — окно «Сверить» и «Применить», права — как у операции, меняющей данные.

Секреты от плагина (новый ключ API):

  • "reveals": true у операции и "reveal": {"title", "value", "note"} в ответе — секрет показывается один раз окну браузера с кнопкой «Скопировать». Токену (API, MCP) хаб такую операцию не даёт, если секрет не уходит сразу в .env.
  • "store": {"pipeline", "env", "value"} в ответе — хаб сам записывает значение в .env конвейера (с историей версий) и убирает его из ответа. Куда — только то, что было в запросе: аргументы to_pipeline и env (операция) или слова to-pipeline КОНВЕЙЕР env ИМЯ (команда сценария). Права (администратор или токен с «pipeline.create» в своих пределах) хаб проверяет до вызова плагина.
  • Значения не попадают ни в журнал действий, ни в журнал задания.

Служебные операции (их нет в ops, хаб вызывает сам):

ОперацияАргументыКогда
_connectname, hub_url, tokenподключение (токен — если просили hub_actions); не нужна — ответьте ошибкой со словом _connect
_checkargv«Проверить» сценарий с командой плагина: ошибка — текстом
_commandargv, dry_run у запросавыполнение команды сценария; text — строки в журнал задания
_dns_zones—provides: dns: {"result": {"zones": ["example.com"]}}
_dns_syncowner, records, previousprovides: dns: привести записи владельца (fly:<маршрут>, site:<домен>) к records, убрав из previous то, чего в records нет

argv — слова команды сценария начиная со слова команды: dns set app.example.com A 203.0.113.10 → ["dns", "set", "app.example.com", "A", "203.0.113.10"].

Для агентов и сценариев ​

«Плагины» → плагин → «Для агентов»: инструменты MCP плагина ровно такими, какими их получает агент (имя, описание, аргументы, обязательные — со звёздочкой), кому они доступны (чтение — любому токену, что меняет — токену с действием «плагин ИМЯ», что удаляет, что отдаёт секрет только в .env конвейера), и команды сценариев с примерами. «Скачать JSON» — тот же формат, что ответ tools/list MCP, плюс команды: отдать агенту или положить рядом с его инструкциями. Если MCP выключен, окно об этом скажет.

Для агентов

Свой плагин: самое короткое ​

Полное руководство — Как написать плагин (оно же examples/AGENTS.md для агентов в репозитории), рабочая заготовка с тестом — examples/_template, проверка — go run ./cmd/nkt-catalog lint -dir examples/<имя>.

Python без зависимостей, один раздел и одна команда:

python
import hashlib, hmac, json, os, time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

SECRET = os.environ["NKT_PLUGIN_SECRET"].encode()

def signed(h, path, body):
    ts = h.headers.get("X-NKT-Timestamp", "0")
    want = hmac.new(SECRET, f"{ts}.{path}.".encode() + body, hashlib.sha256).hexdigest()
    return abs(time.time() - int(ts)) < 300 and hmac.compare_digest(want, h.headers.get("X-NKT-Signature", ""))

MANIFEST = {"protocol": 1, "name": "hello", "title": "Hello", "version": "0.1",
            "ops": [{"name": "info", "read_only": True, "mcp": True, "description": "Say hello"}],
            "pages": [{"id": "info", "title": "Hello", "op": "info"}],
            "commands": [{"word": "hello", "usage": "hello ИМЯ"}]}

class H(BaseHTTPRequestHandler):
    def reply(self, obj, code=200):
        raw = json.dumps(obj).encode()
        self.send_response(code); self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(raw))); self.end_headers(); self.wfile.write(raw)
    def do_GET(self):
        if not signed(self, self.path, b""): return self.reply({"error": "signature"}, 401)
        self.reply(MANIFEST)
    def do_POST(self):
        body = self.rfile.read(int(self.headers.get("Content-Length", 0)))
        if not signed(self, self.path, body): return self.reply({"error": "signature"}, 401)
        req = json.loads(body)
        if req["op"] == "info":
            return self.reply({"ok": True, "view": {"type": "text", "text": "Hello from the plugin"}})
        if req["op"] in ("_check", "_command"):
            argv = req["args"]["argv"]
            if len(argv) != 2: return self.reply({"ok": False, "error": "hello ИМЯ"})
            return self.reply({"ok": True, "text": f"hello, {argv[1]}" + (" (dry run)" if req.get("dry_run") else "")})
        self.reply({"ok": False, "error": f"no op {req['op']}"})

ThreadingHTTPServer(("0.0.0.0", 9180), H).serve_forever()

В compose — ports: ["127.0.0.1:9180:9180"] и секрет из .env. Полный пример — плагин Technitium DNS в examples/technitium.