Écrire un plugin¶
Cette page mène un développeur d'un clone d'Evey à un plugin qui tourne sur un serveur local, puis à un paquet signé que d'autres administrateurs peuvent installer. Le tutoriel de la section 3 est le chemin principal ; le reste est de la référence à laquelle vous reviendrez. Les appels et les champs eux-mêmes sont dans la Référence de l'API des plugins.
flowchart LR
subgraph server["Serveur Evey"]
host["HostService<br/>vérifie chaque appel selon<br/>les permissions déclarées"]
ui["Interface web<br/>emplacements des plugins"]
end
subgraph plugin["Plugin, dans sa sandbox"]
backend["PluginService<br/>votre processus backend"]
front["Module d'interface<br/>iframe isolée"]
end
backend <-->|"gRPC sur un socket Unix"| host
front <-->|"pont postMessage"| ui
host -->|"Fetch : hôtes déclarés uniquement"| net(("Internet"))
Un plugin est un ZIP avec un plugin.yaml à sa racine et un processus
backend que le serveur démarre dans une sandbox. Le processus sert un petit
service gRPC sur un socket Unix et rappelle le serveur sur un autre. Il peut
aussi embarquer un module d'interface, affiché dans un cadre isolé de la page,
et ne communique avec l'extérieur que par l'intermédiaire du serveur, vers
les hôtes qu'il a déclarés.
1. Prérequis¶
| Outil | Pourquoi |
|---|---|
| Go 1.27, Node.js 22 avec pnpm, Docker avec Compose | La pile de développement (make dev) construit le serveur et l'application web. |
bubblewrap (bwrap) |
Seulement pour les modes de sandbox bubblewrap et strict. Le mode auto par défaut utilise Landlock et n'a besoin de rien sur un noyau 5.13 ou plus récent. |
| cosign | Pour signer votre paquet avant de le distribuer (section 7). Inutile pour développer. |
Choisissez le langage de votre plugin parmi les environnements que le serveur
sait lancer : python, nodejs, php, ruby, lua, bash et sh ont
besoin de leur interpréteur dans l'image, go livre un binaire compilé et
demande la permission native-binary. L'image par défaut embarque
Python, Node.js, PHP, Ruby et Lua ; l'image -nodejs garde Python et Node.js
et l'image -lean ne garde que Python. Un manifeste qui nomme un environnement absent de l'image est refusé
à l'installation, avec la liste des environnements disponibles.
Le contrat gRPC tient dans un fichier, proto/plugin/v1/plugin_api.proto. Seul
le SDK Python livre des stubs générés ; les autres langages sous
sdk/<langage>/ contiennent le .proto. Le serveur copie les fichiers de
votre langage à côté de votre point d'entrée au démarrage du plugin. L'image
fournit grpcio aux plugins Python, qui n'ont donc rien à embarquer. Un plugin
Node.js doit embarquer son node_modules (@grpc/grpc-js et
@grpc/proto-loader) dans le paquet. Pour un autre langage, générez les stubs
avec protoc et livrez-les dans le ZIP :
protoc --proto_path=proto \
--<lang>_out=OUT_DIR --grpc_<lang>_out=OUT_DIR \
proto/plugin/v1/plugin_api.proto
make plugin-sdk régénère les stubs fournis ; chaque sdk/<lang>/README.md
nomme le plugin gRPC dont son langage a besoin.
2. Lancer Evey en local¶
git clone git@github.com:eveyproject/evey.git
cd evey
cp .env.dev.example .env.dev # puis renseignez MEDIA_DIR, EVEY_DB_PASSWORD, MEILI_MASTER_KEY
make setup # une fois : ajoute evey.test à /etc/hosts
make dev # postgres, meilisearch, le serveur (Air), l'application web (Vite)
Ouvrez https://evey.test. L'assistant d'installation crée le compte
administrateur au premier lancement, comme décrit dans
Première configuration. Les fichiers des
plugins arrivent dans ./evey-data/plugins/<id-du-plugin>/, monté en
/data/plugins dans le conteneur.
3. Votre premier plugin, pas à pas¶
L'exemple examples/plugins/nodejs-example est le plus petit plugin complet :
il sert PluginService, se connecte à HostService, et écrit une ligne dans
son journal quand le serveur le démarre. Nous le copions, changeons une chose,
et l'installons.
-
Copiez l'exemple.
-
Donnez-lui un nom. Modifiez
plugin.yaml:plugin.yamlname: hello-plugin # unique, en kebab-case kind: plugin version: 1.0.0 # semver : à incrémenter à chaque envoi description: Dit bonjour dans son journal author: vous language: nodejs # l'environnement que le serveur doit avoir entrypoint: node main.js # la ligne de commande que le serveur lance permissions: - api.library # laisse le backend lire les médiaslanguageindique au serveur quel environnement vérifier et quels stubs copier ;entrypointest ce qu'il lance, dans le dossier du plugin, avec l'environnement de l'étape suivante. -
Lisez
main.js. Le serveur passe trois variables au processus :EVEY_PLUGIN_ID,EVEY_PLUGIN_SOCK(le socket sur lequel votre processus doit écouter, en servantPluginService) etEVEY_HOST_SOCK(le socket auquel vous vous connectez pour appelerHostService). L'exemple chargeplugin_api.protodepuis son propre dossier, où le serveur l'a copié, et implémente les cinq méthodes dePluginService. Changez le message dansInitialize: -
Compressez-le. Le manifeste doit être à la racine de l'archive, pas dans un sous-dossier :
-
Installez-le. Dans Administration → Plugins, Installer un plugin, choisissez le ZIP puis Aperçu et installation : l'aperçu liste
api.libraryet le résultat de l'analyse statique. Par l'API, le même envoi s'écrit : -
Lisez le journal. Sur la carte du plugin, Logs montre la ligne écrite par
Initialize.docker compose logs eveymontre le côté serveur du même démarrage, sous l'identifiant du plugin. -
Itérez. Modifiez le code, incrémentez
version, compressez de nouveau et utilisez Mettre à jour depuis la source… sur la carte. Un envoi avec la même version ou une version inférieure est refusé ; une nouvelle permission dans le manifeste déclenche l'étape de consentement que l'administrateur doit approuver.
Vous avez là tout le contrat backend. À partir d'ici, un plugin peut être
étendu de trois façons : appeler le serveur (HostService, section 4),
afficher une interface (section 5), et accéder à l'extérieur (section 6).
4. Appeler le serveur depuis le backend¶
Chaque appel à HostService porte l'identité du plugin : le serveur vérifie
le processus sur le socket par son PID, et un intercepteur refuse tout appel
dont le manifeste ne déclare pas la permission. Trois groupes n'ont besoin
d'aucune permission :
Logécrit dans le journal du plugin, affiché sur sa carte ;GetConfiglit les valeurs que l'administrateur a saisies dans Config (déclarées dansconfig_schema),SetConfiglaisse le plugin y conserver ses propres clés ;GetData/SetData/DeleteData/ListDataKeysest le stockage clé/valeur privé du plugin, effacé à la désinstallation.
GetMediaItem et ListMediaItems demandent api.library ou media.read ;
Fetch demande network. Le tableau complet, avec les méthodes déclarées
dans le contrat mais pas encore servies, est dans la
référence de l'API.
5. Ajouter une interface¶
Une interface est un module JavaScript que la page charge dans un cadre isolé, dans l'un des emplacements que le serveur autorise. Déclarez où sont les fichiers et où va le module :
web_root: dist # le dossier servi comme ressources statiques
frontend:
- slot: home.section # voir la liste des emplacements
src: dist/index.js # doit être sous web_root
Le module reçoit un pont window.evey : evey.onInit lui remet la
configuration du plugin, la langue, le thème et la surface sur laquelle il
s'affiche ; evey.resize, evey.toast et evey.navigate agissent sur la
page dans des limites fixes ; et, quand le manifeste déclare prefs ou
public, evey.prefs.* et evey.media.* atteignent le serveur par un jeton
de courte durée que l'hôte détient pour le cadre.
examples/plugins/weather est un widget complet avec une étape de build ;
examples/plugins/data-demo montre prefs, public et l'emplacement
admin.nav. Le pont et les emplacements sont décrits dans la
référence de l'API.
6. Accéder à l'extérieur¶
Un processus de plugin ne peut ouvrir aucun socket vers Internet, dans tous
les modes où le lanceur tourne (basic sans le lanceur interdit seulement execve). Le seul accès possible est l'appel Fetch de
HostService : le serveur vérifie la cible contre le manifeste, puis fait la
requête lui-même.
permissions:
- network
external_fetch:
- api.open-meteo.com # noms d'hôte nus : pas de schéma, pas de joker, pas d'IP
network sans external_fetch est refusé à l'installation. À l'exécution, le
courtier accepte http et https, GET et HEAD, répond jusqu'à 8 Mio en
20 secondes, et refuse les adresses privées ou réservées même derrière une
redirection. Le cadre de l'interface reçoit la même liste d'hôtes comme
connect-src, donc un widget peut appeler ces hôtes directement en https.
Les règles sont dans
Permissions et emplacements.
Les hôtes sont un contrat avec l'administrateur
L'administrateur voit la liste des hôtes à la mise à jour et dans le Tableau de bord sécurité, et doit approuver tout ajout à la mise à jour. Limitez-la à ce dont le plugin a besoin, et indiquez dans votre README ce que chaque hôte reçoit.
7. Signer son plugin¶
Ce que prouve une signature
Une signature atteste que le ZIP que l'administrateur installe est, octet
pour octet, celui que vous avez publié. Votre clé publique voyage dans
le paquet (cosign.pub à la racine du ZIP), et Evey épingle son empreinte
sur le plugin à l'installation : une mise à jour signée avec une autre clé,
ou qui n'est plus signée, est refusée tant que l'administrateur n'a pas
accepté le changement. Elle n'indique rien de ce que fait le plugin : les
permissions, les hôtes et l'analyse de l'aperçu s'appliquent toujours.
Evey la vérifie hors ligne et dans son propre processus (ECDSA P-256 sur
le SHA-256 du ZIP, le format que produit cosign sign-blob --key) : pas
de binaire cosign sur le serveur, pas de réseau, pas de journal de
transparence.
Vous n'avez besoin de cosign que sur votre machine.
-
Une fois : générez une paire de clés.
-
À chaque version : placez votre clé publique à la racine du plugin, compressez, puis signez le fichier exact que vous distribuerez.
-
Vérifiez avant de livrer, avec la même vérification que fait le serveur :
-
Publiez les deux fichiers ensemble :
hello-plugin.zip(votre clé publique est dedans) ethello-plugin.zip.sig, côte à côte, pour qu'une installation par URL trouve la signature à<url>.sig. Mettez l'empreinte de la clé (sha256:..., affichée par Evey sur la carte du plugin) dans votre README et vos notes de version, pour que les administrateurs puissent la comparer avant de faire confiance à la clé.
Sur l'instance qui l'installe, l'administrateur choisit le ZIP dans
Administration → Plugins et, dans Fichier signature (.zip.sig,
optionnel), son .zip.sig ; par l'API, la signature est le champ sig de
l'envoi. Evey vérifie le paquet avec la clé qu'il porte et affiche l'un de :
- Signé par <éditeur> : la clé est dans la liste des Éditeurs de confiance de l'instance.
- Signé - clé inconnue sha256:... : la signature est bonne, la clé n'est pas encore de confiance. Un clic, Faire confiance à cette clé, l'ajoute à la liste sous un nom.
- Non signé, ou Signature invalide.
La politique décide de la suite : EVEY_PLUGINS_COSIGN_POLICY=warn (défaut)
enregistre le verdict et laisse passer toute installation ; enforce n'admet
que les clés de confiance. Quelle que soit la politique, une mise à jour dont
la clé diffère de la version installée, ou qui abandonne la signature, est
refusée tant que l'administrateur n'a pas coché L'éditeur a changé,
j'accepte la nouvelle clé dans la fenêtre de mise à jour.
EVEY_PLUGINS_COSIGN_PUBLIC_KEY est facultative : elle ajoute une clé à la
liste de confiance au démarrage (marquée comme celle de l'opérateur du
serveur).
Erreurs fréquentes :
- Tout changement du ZIP invalide la signature, même recompresser les mêmes fichiers. Signez le fichier que vous distribuez, et signez de nouveau à chaque version.
- Gardez la même clé d'une version à l'autre. Une nouvelle clé est un nouvel éditeur pour chaque administrateur qui vous a installé : on lui demandera de l'accepter. Changez de clé volontairement, et dites-le dans vos notes de version.
- Seules les clés ECDSA P-256 sont acceptées, ce que produit
cosign generate-key-pair. La signature sans clé et les références KMS ne sont pas vérifiées. - Un
.sigsanscosign.pubdans le ZIP, ou l'inverse, compte comme Non signé. - Une mauvaise clé, un ZIP altéré et un
.sigmanquant sont tous traités comme une signature en échec : sousenforce, l'installation ou la mise à jour est refusée.
8. Ce que vérifie l'installateur, dans l'ordre¶
Chaque étape peut refuser le paquet ; l'administrateur voit la raison, et le journal du serveur contient le détail.
- Manifeste.
plugin.yamlest lu directement dans le ZIP : champs obligatoires, semver, permissions et capacités connues, emplacements dans la liste autorisée, entréesexternal_fetchqui sont des noms d'hôte nus,web_rootprésent quandfrontendest déclaré. Lelanguagedéclaré doit être un environnement de l'image. - Version et escalade. Pour un plugin déjà installé, la nouvelle version
doit être supérieure, et toute nouvelle permission, capacité, emplacement
ou hôte arrête l'installation ici tant que l'administrateur n'a pas
approuvé la différence (
accept_permission_changes=truesur la route d'envoi). - Signature et empreinte. La signature est vérifiée quand une clé
publique est configurée ; un échec ne bloque que sous
enforce. L'empreinte du ZIP est enregistrée, et vérifiée de nouveau quand une mise à jour prévisualisée plus tôt est appliquée. - Extraction dans un dossier de transit : au plus 5 000 entrées, 100 Mo par fichier, 500 Mo au total, un taux de compression de 100:1, aucun lien symbolique, aucun chemin qui sort de l'archive. Une mise à jour ne touche jamais la version en cours tant que toutes les étapes suivantes n'ont pas réussi.
- Thèmes seulement : la feuille de style est assainie et réécrite, et un
theme.jsqui correspond à un motif interdit fait refuser le paquet. - Analyse statique de chaque fichier texte : un constat critique refuse
le paquet ; les avertissements
deserializationetobfuscationle refusent aussi, etnative_code, les binaires compilés et les archives embarquées le refusent sauf si le manifeste déclarenative-binary. - Enregistrement et démarrage. Le dossier de transit est mis en place, la ligne est écrite, et le processus est démarré.
Ce que cherche l'analyse est listé dans Sandbox des plugins et modèle de menace. Les commentaires sont retirés avant l'analyse : une ligne mise en commentaire n'est pas signalée.
9. Dans quelles conditions tourne votre processus¶
Le serveur démarre le processus avec une priorité processeur abaissée, un
plafond de mémoire, une limite de processus et de threads, et une taille
maximale pour son dossier ; un moniteur arrête un plugin qui les dépasse. Les
défauts sont 256 Mio, 64 processus et 100 Mio, ajustables par
l'administrateur dans Administration → Plugins → Limites de ressources. Sans la
permission subprocess, le processus ne peut pas créer d'enfants, quel que
soit le langage ; les threads fonctionnent.
Dans tous les modes où le lanceur tourne, la sandbox confine le système de fichiers au dossier du plugin et bloque tout
socket réseau (basic sans le lanceur interdit seulement execve). Le mode en vigueur dépend de l'hôte, pas du
plugin : Sandbox des plugins et modèle de menace
les décrit, et Mise en place de la sandbox des plugins
indique à un administrateur lequel choisir.
10. Distribuer un plugin¶
Il n'y a pas de catalogue : un administrateur installe un plugin en envoyant son ZIP ou en collant son URL dans le panneau d'administration, depuis l'endroit où vous le publiez (une release sur une forge, votre propre site).
- Livrez le ZIP, son
.siget votrecosign.pubensemble (section 7). - Indiquez les permissions et les hôtes
external_fetchdans votre README : l'administrateur voit les permissions à l'installation, et les hôtes à la mise à jour et dans le Tableau de bord sécurité, et doit les approuver ; une surprise à cet endroit est la première cause de refus. - Pour faire figurer votre plugin parmi les exemples fournis, ouvrez une pull
request contre
examples/plugins/avec la source et un README. Les exemples sont relus et construits depuis la source ; une inscription n'est pas une recommandation et ne porte aucune signature du projet.
11. Dépannage¶
| Symptôme | Cause | Solution |
|---|---|---|
plugin requires "nodejs" runtime which is not installed in this image |
La variante d'image n'a pas l'interpréteur. | Utilisez l'image par défaut ou -nodejs, ou écrivez le plugin en Python. |
manifest: external_fetch entry "*.tmdb.org" is a wildcard - use explicit hostnames |
Les jokers sont refusés à l'installation, sans exception. | Listez chaque nom d'hôte nécessaire (api.tmdb.org, image.tmdb.org). |
manifest: frontend slot "admin.section" not allowed |
L'emplacement est hors de la liste autorisée. | Prenez un emplacement de la liste ; admin.nav est le seul emplacement de la zone d'administration. |
manifest: network permission requires non-empty external_fetch |
network déclaré sans hôtes. |
Ajoutez les hôtes, ou retirez la permission. |
plugin already installed at version 1.0.0, new version must be higher |
Le ZIP porte la version déjà installée. | Incrémentez version, ou désinstallez d'abord. |
signature does not match the plugin or the configured key |
Le .sig a été fait avec une autre clé, ou le ZIP a changé après la signature. |
Re-signez le ZIP exact avec la bonne clé, ou corrigez EVEY_PLUGINS_COSIGN_PUBLIC_KEY. |
plugin blocked: theme.js contains forbidden pattern |
Le scanner du script de thème a trouvé une API interdite. | Un thème ne peut appeler que evey.theme.* : voir Thèmes. |
Le journal se termine par memory quota exceeded (... MB > ... MB) |
Le moniteur a arrêté le plugin au-delà de sa limite de mémoire. | Réduisez la consommation, ou montez Mémoire max (Mio) dans Limites de ressources. |
bwrap: ... permission denied |
Mode bubblewrap dans Docker, bloqué par le profil seccomp ou AppArmor du conteneur. |
Gardez le mode auto par défaut. Sinon, voir Mise en place de la sandbox des plugins. |
| Le module d'interface n'apparaît pas | Le plugin est désactivé, l'emplacement est mal orthographié, ou src n'est pas sous web_root. |
Vérifiez l'état de la carte, le nom de l'emplacement, et les requêtes /api/v1/plugins/<id>/assets/... dans le panneau réseau du navigateur. |