Skip to content

Как написать плагин хаба nkt — руководство для ИИ-агента ​

Это руководство — для агента (и человека), который пишет плагин для каталога nkt. Прочитайте его целиком до первой строки кода: здесь правила, которые проверяют nkt-catalog lint, тесты и CI, и то, что они проверить не могут, но обязано соблюдаться.

0. Нужен ли плагин ​

  • Устройство по SSH, HTTP API или SNMP, которое достаточно читать командами (роутер, NAS, принтер, ИБП) — не пишите плагин: опишите тип устройства профилем JSON плагина connector («Хосты плагинов» → «Connector» в справке, примеры — BUILTIN в examples/connector/deploy/nkt-plugin.py).
  • Сервис со своим API (Forgejo, LiteLLM, Home Assistant, облако) или устройство, которому нужна логика сложнее профиля, — плагин.

1. С чего начать ​

Скопируйте заготовку — рабочий плагин с тестом:

sh
cp -r examples/_template examples/<имя>

Образцы по виду плагина:

ВидОбразец
API сервиса: разделы, формы, команды, MCPlitellm, home-assistant
Устройства в «Хостах» (provides: hosts)mikrotik (HTTPS API), windows (SSH)
DNS для хаба (provides: dns)technitium, cloudflare, yandex-cloud
Секрет один раз / в .env конвейера (reveal, store)litellm (key_generate)
Плагину нужен Docker, записи DNS для сервисаdocker-mailserver

2. Состав папки ​

ФайлЧто
deploy/nkt-plugin.pyплагин: Python 3, только стандартная библиотека
deploy/nkt-plugin.jsonпакет для каталога и установщика хаба
deploy/test_plugin.pyтест на подделке — обязателен, без него плагин не попадёт в каталог
README.md, README.en.mdсправка: что умеет, что нужно на стороне сервиса, как поставить
screens/ru/*.png, screens/en/*.pngскриншоты — показываются в каталоге хаба и на сайте
pipeline*.yaml, nkt-example.jsonнеобязательно: пример конвейера, который выкладывает сам сервис

nkt-plugin.json:

json
{
  "name": "forgejo", "version": "1.0.0", "min_nkt": "1.16.0", "protocol": 1,
  "title": {"ru": "…", "en": "…"}, "description": {"ru": "…", "en": "…"},
  "run": "python3", "file": "nkt-plugin.py", "port": 9270,
  "docker": false,
  "settings": [
    {"env": "FORGEJO_URL", "title": {"ru": "…", "en": "…"}, "default": "http://127.0.0.1:3000", "hint": {"ru": "…", "en": "…"}},
    {"env": "FORGEJO_TOKEN", "title": {"ru": "…", "en": "…"}, "secret": true, "required": true}
  ]
}
  • name — ^[a-z][a-z0-9-]{1,31}$; так же называется служба nkt-plugin-<name> и инструменты MCP <name>_<операция>.
  • version — X.Y.Z и ровно то же, что VERSION в коде. Любое изменение файлов плагина — новая версия: CI сверяет с опубликованным каталогом и не соберёт его с той же версией.
  • min_nkt — самая старая версия nkt, с которой плагин работает (поля протокола, которые он использует, — см. справку «Плагины»).
  • port — свободный среди плагинов каталога (lint -all проверяет).
  • settings — форма установки; переменные окружения службы. Имена — A-Z0-9_, не NKT_ (это хаба). Всё, что похоже на токен, пароль или ключ, — "secret": true: хаб такие значения не показывает.
  • docker: true — службе нужна группа docker (это права root): хаб предупредит администратора до установки. Без нужды не ставьте.

3. Протокол — коротко ​

Полностью — справка «Плагины» → «Протокол». Плагин — HTTP-сервер на NKT_PLUGIN_LISTEN:NKT_PLUGIN_PORT (хаб ставит 127.0.0.1 и порт из пакета).

  • Подпись. Каждый запрос: X-NKT-Timestamp (секунды Unix), X-NKT-Signature = hex HMAC-SHA256 строки <отметка>.<путь>.<тело> секретом NKT_PLUGIN_SECRET, X-NKT-Lang (ru/en). Отметка дальше 300 с или подпись не сошлась — 401. Сравнение — hmac.compare_digest.
  • GET /nkt/manifest — манифест на языке запроса.
  • POST /nkt/call {op, args, dry_run, user} → {ok, error?, text?, result?, view?, reveal?, store?}. Неизвестная операция — {"ok": false, "error": "no op …"} с HTTP 200, не исключение.
  • Служебные операции (в манифесте их нет): _connect (плагин подключён), _check (разобрать команду сценария, ничего не делая), _command (выполнить её; dry_run — только сказать, что будет), _dns_zones / _dns_sync (provides: dns), _host_* (provides: hosts).

4. Манифест ​

json
{"protocol": 1, "name": "…", "title": "…", "version": "…", "description": "…",
 "ops": [{"name": "repos", "title": "…", "read_only": true, "mcp": true, "description": "…"},
         {"name": "repo_create", "mcp": true, "description": "…",
          "input": {"type": "object", "properties": {"name": {"type": "string"}}, "required": ["name"]}},
         {"name": "repo_delete", "mcp": true, "destructive": true, "description": "…", "input": {…}}],
 "pages": [{"id": "repos", "title": "…", "op": "repos"}],
 "commands": [{"word": "git", "usage": "git repos | git mirror-sync РЕПО", "example": "…"}]}
  • Операции (^[a-z][a-z0-9_]{0,47}$). read_only — видна всем, кто видит хаб, и токенам чтения; без него — администратору или токену с действием «плагин ИМЯ». destructive — удаляет (агент и токены видят это). mcp — инструмент <name>_<op> для агентов; описание на английском обязательно — его читает агент. input — JSON Schema аргументов. reveals — отдаёт секрет (см. п. 5).
  • Разделы (pages) — операция только для чтения, которая возвращает вид. hidden — открывается только кнопкой open из другого раздела.
  • Виды: {"type": "table", "title", "note", "columns": [{key, title}], "rows": [{"cells": {key: значение}, "actions": [...]}], "actions": [...]}, {"type": "text", "text": "…"}, {"type": "sections", "items": [вид, …]}.
  • Кнопки (actions): op + args + fields (форма: text, textarea, number, select с options, bool, password) + confirm + danger; open — раздел с args; edit — правка документа с диффом и историей в хабе (get → result.text, put ← args.text, doc); dns — записи, которые хаб поставит через DNS-плагин.
  • Команды сценариев — word (^[a-z][a-z0-9-]{1,23}$, не слово языка сценариев) и usage; выполняет _command с argv — словами после команды, включая само слово.
  • provides: ["hosts"] + host_types — устройства в «Хостах»: поля формы (обязательно address; password — секрет; fingerprint заполняет хаб), sections, actions, exec (язык команд), icon (значок: mikrotik, openwrt, ups, camera, homeassistant, printer, switch, device…). Операции _host_* — справка «Хосты плагинов» → «Свой тип устройств».
  • hub_actions — права в самом хабе (выпуск токена плагину). Просите только если без них нельзя: администратор видит запрос.

5. Секреты ​

  • Настройки (secret: true) — только окружение службы: файл /etc/nkt-plugins/<name>.env, 0600, без истории. Плагин читает их из os.environ и никогда не пишет в журнал, ответы, тексты ошибок.
  • Учётки устройств (provides: hosts) хаб передаёт в каждом вызове (args.conn) — не сохраняйте их на диск.
  • Новый секрет для человека (ключ API, токен раннера): операция "reveals": true, ответ {"ok": true, "reveal": {"title", "value", "note"}} — хаб покажет его один раз только окну браузера.
  • Секрет для агента или сценария — только в .env конвейера: аргументы to_pipeline и env (у команды — слова to-pipeline P env K), ответ {"store": {"pipeline", "env", "value"}}; хаб проверит права и запишет сам. Агенту значение не возвращается никогда.
  • Аргументы с именами pass, secret, token, key хаб в журнале действий заменяет на *** — но не полагайтесь на это: не принимайте секреты аргументами без нужды.
  • Пароль для внешней программы — через окружение или stdin, не в командной строке (её видят все процессы машины): SSH_ASKPASS (windows, connector), -e ИМЯ у docker exec (docker-mailserver).

6. Безопасность — обязательно ​

  1. Проверяйте каждый вход до использования: имя, путь, адрес, номер — регулярным выражением или разбором (ipaddress, int). Только то, что прошло, идёт дальше. Слишком длинное и с управляющими символами — отказ.
  2. Никакой оболочки. subprocess.run([...]) списком аргументов, без shell=True, os.system, eval, exec (lint проверяет). Перед позиционными аргументами из ввода — -- (ssh … -l USER -- HOST), имя пользователя — отдельным аргументом. Внешние программы — с таймаутом.
  3. Пути и адреса API — urllib.parse.quote(значение, safe="") для каждой части пути; хост — из настроек, не из аргументов вызова.
  4. TLS. К чужому серверу со своим сертификатом — режим «запомнить отпечаток» (pin, см. mikrotik, connector): отпечаток сохраняет хаб в учётках, плагин сверяет его каждый раз. Не отключайте проверку сертификатов для всего процесса.
  5. Ответы сервиса — тоже ввод: JSON — json.loads, XML — без внешних сущностей (стандартный xml.etree), размер — с пределом.
  6. Ошибки. Человеку — Fail с текстом на его языке (без секретов и без трассировки); неожиданное — {"ok": false, "error": "plugin error: <тип>"}, в журнал — только тип.
  7. Сухой прогон. Каждая операция, которая меняет, при dry_run только говорит, что сделает: (сухой прогон) …. _check — только разбирает, никуда не ходит.
  8. Журнал службы (print(..., flush=True)) — кто и что сделал, без значений секретов и без тел запросов.
  9. Только стандартная библиотека Python: хаб ставит плагин файлом, без pip. Нужна внешняя программа (ssh, openssl) — проверьте shutil.which и дайте понятную ошибку, как поставить.
  10. Плагин запускается без настроек (их задают после установки) и отвечает понятной ошибкой «не задан …» при вызове.

7. Тексты ​

Все строки интерфейса — в словаре T на ru и en, язык — из X-NKT-Lang. Описания операций MCP — на английском (их читают агенты). Русский текст — без канцелярита, кавычки «ёлочки»; в английском — без ASCII-кавычек " внутри строк (используйте “ ”).

8. Тест на подделке (deploy/test_plugin.py) ​

Подделка — HTTP-сервер (или ssh, SNMP — как в connector) с ответами настоящего API по его документации и проверкой токена. Плагин запускается отдельным процессом (python3 -I nkt-plugin.py), вызовы — подписанные. Обязательно проверить:

  • манифест на обоих языках;
  • каждый раздел (вид) и форму;
  • проверку ввода: опасные значения (../x, a b, -oProxyCommand=…, x;rm) — отказ;
  • сухой прогон ничего не меняет в подделке;
  • изменение доходит до подделки с нужными аргументами;
  • команды сценария: _check и _command;
  • неверный токен сервиса — понятная ошибка;
  • неверная подпись — 401;
  • в выводе плагина нет ни секрета подписи, ни токенов (проверка в tearDownClass).

Добавьте вызов теста в examples/plugins_test.go (Test<Имя>PluginPython).

9. Проверки и каталог ​

sh
python3 -I examples/<имя>/deploy/test_plugin.py
go run ./cmd/nkt-catalog lint -dir examples/<имя>     # состав, пакет, код, живой манифест
go run ./cmd/nkt-catalog lint -all examples           # и порты всех плагинов
go test ./examples ./internal/catalog

lint запускает плагин без настроек и проверяет манифест той же функцией, что хаб при подключении, подпись (401 без подписи и со старой отметкой), отказ на неизвестную операцию, отсутствие секрета в журнале. CI (.github/workflows/pages.yml) перед сборкой каталога гоняет lint и тест каждого плагина: не прошёл — плагина нет в каталоге, остальные выходят.

10. Чек-лист перед коммитом ​

  • [ ] Папка examples/<имя>/ из заготовки; name, порт, server_version, log — свои.
  • [ ] version в JSON = VERSION в коде; при изменении плагина — новая.
  • [ ] Настройки с секретами — "secret": true; секретов нет в журнале, ответах, ошибках.
  • [ ] Каждый вход проверен до использования; нет shell=True, os.system, eval.
  • [ ] Каждая меняющая операция поддерживает dry_run; _check ничего не вызывает.
  • [ ] Все тексты на ru и en; описания MCP — на английском.
  • [ ] Тест на подделке покрывает п. 8 и вызывается из examples/plugins_test.go.
  • [ ] nkt-catalog lint без замечаний.
  • [ ] README.md и README.en.md: что умеет, что нужно на стороне сервиса (права токена), как поставить, безопасность.
  • [ ] Скриншоты в screens/ru/ и screens/en/, если есть интерфейс.

11. Свой плагин без каталога ​

Плагин не для общего каталога — загрузите архив его папки в хаб: «Плагины» → «Каталог» → «Загрузить свой плагин» (только администратор). Хаб проверит его тем же статическим lint, покажет код и поставит на хосты как плагин каталога. Пример — examples/_samples/url-check.

12. Вживую (по желанию) ​

Запустите плагин с NKT_PLUGIN_SECRET, NKT_PLUGIN_PORT, NKT_PLUGIN_LISTEN=127.0.0.1 и настройками и подключите его к тестовому хабу: «О системе» → «Плагины» → «Подключить» → хост localhost, порт, секрет. Разделы, команды и «Для агентов» появятся сразу.