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