Zum Inhalt

Ein Plugin schreiben

Diese Seite führt einen Entwickler von einem Klon von Evey zu einem Plugin, das auf einem lokalen Server läuft, und dann zu einem signierten Paket, das andere Administratoren installieren können. Die Anleitung in Abschnitt 3 ist der direkte Weg; der Rest ist Nachschlagematerial, zu dem Sie zurückkehren werden. Die Aufrufe und Felder selbst stehen in der Plugin-API-Referenz.

flowchart LR
  subgraph server["Evey-Server"]
    host["HostService<br/>prüft jeden Aufruf gegen<br/>die angegebenen Berechtigungen"]
    ui["Weboberfläche<br/>Plugin-Slots"]
  end
  subgraph plugin["Plugin, in seiner Sandbox"]
    backend["PluginService<br/>Ihr Backend-Prozess"]
    front["Frontend-Modul<br/>isolierter iframe"]
  end
  backend <-->|"gRPC über einen Unix-Socket"| host
  front <-->|"postMessage-Brücke"| ui
  host -->|"Fetch: nur angegebene Hosts"| net(("Internet"))

Ein Plugin ist ein ZIP mit einer plugin.yaml auf oberster Ebene und einem Backend-Prozess, den der Server in einer Sandbox startet. Der Prozess bietet einen kleinen gRPC-Dienst auf einem Unix-Socket an und ruft den Server über einen anderen zurück. Es kann auch ein Oberflächenmodul mitbringen, das in einem isolierten Frame der Seite dargestellt wird. Mit externen Diensten kommuniziert es nur über den Server, und nur mit den Hosts, die es angegeben hat.

1. Voraussetzungen

Werkzeug Wozu
Go 1.27, Node.js 22 mit pnpm, Docker mit Compose Die Entwicklungsumgebung (make dev) baut den Server und die Web-App.
bubblewrap (bwrap) Nur für die Sandbox-Modi bubblewrap und strict. Der Standardmodus auto nutzt Landlock und braucht ab Kernel 5.13 nichts weiter.
cosign Um Ihr Paket vor der Verteilung zu signieren (Abschnitt 7). Zum Entwickeln nicht nötig.

Wählen Sie die Sprache Ihres Plugins unter den Laufzeiten, die der Server starten kann: python, nodejs, php, ruby, lua, bash und sh brauchen ihren Interpreter im Image; go bringt eine kompilierte Binärdatei mit und braucht die Berechtigung native-binary. Das Standard-Image enthält Python, Node.js, PHP, Ruby und Lua. Das Image -nodejs behält Python und Node.js, das Image -lean nur Python. Ein Manifest, das eine Laufzeit nennt, die dem Image fehlt, wird bei der Installation abgelehnt, mit der Liste der verfügbaren Laufzeiten.

Die gRPC-Schnittstelle ist in einer einzigen Datei definiert, proto/plugin/v1/plugin_api.proto. Nur das Python-SDK liefert erzeugte Stubs; die anderen Sprachen unter sdk/<language>/ enthalten die .proto. Der Server kopiert die Dateien Ihrer Sprache beim Start des Plugins neben Ihren Einstiegspunkt. Das Image stellt Python-Plugins grpcio bereit, sie müssen also nichts mitliefern. Ein Node.js-Plugin muss sein node_modules (@grpc/grpc-js und @grpc/proto-loader) im Paket mitliefern. Für eine andere Sprache erzeugen Sie die Stubs mit protoc und legen sie ins ZIP:

protoc --proto_path=proto \
       --<lang>_out=OUT_DIR --grpc_<lang>_out=OUT_DIR \
       proto/plugin/v1/plugin_api.proto

make plugin-sdk erzeugt die mitgelieferten Stubs neu. Jede sdk/<lang>/README.md nennt das gRPC-Plugin, das ihre Sprache braucht.

2. Evey lokal starten

git clone git@github.com:eveyproject/evey.git
cd evey
cp .env.dev.example .env.dev   # dann MEDIA_DIR, EVEY_DB_PASSWORD, MEILI_MASTER_KEY setzen
make setup                      # einmalig: trägt evey.test in /etc/hosts ein
make dev                        # postgres, meilisearch, der Server (Air), die Web-App (Vite)

Öffnen Sie https://evey.test. Der Einrichtungsassistent legt beim ersten Start das Administratorkonto an, wie in Ersteinrichtung beschrieben. Plugin-Dateien liegen in ./evey-data/plugins/<plugin-id>/, im Container als /data/plugins eingehängt.

3. Ihr erstes Plugin, Schritt für Schritt

Das Beispiel examples/plugins/nodejs-example ist das kleinste vollständige Plugin: Es bedient PluginService, verbindet sich mit HostService und schreibt eine Zeile in sein Protokoll, wenn der Server es startet. Wir kopieren es, ändern eine Sache und installieren es.

  1. Kopieren Sie das Beispiel.

    cp -r examples/plugins/nodejs-example ~/hello-plugin
    cd ~/hello-plugin
    
  2. Geben Sie ihm einen Namen. Bearbeiten Sie plugin.yaml:

    plugin.yaml
    name: hello-plugin          # eindeutig, kebab-case
    kind: plugin
    version: 1.0.0              # semver: bei jedem Hochladen erhöhen
    description: Says hello in its log
    author: you
    language: nodejs            # die Laufzeit, die der Server haben muss
    entrypoint: node main.js    # die Befehlszeile, die der Server startet
    permissions:
      - api.library             # erlaubt dem Backend, Medien zu lesen
    

    language sagt dem Server, welche Laufzeit er prüfen und welche Stubs er kopieren soll. entrypoint ist das, was er startet, im Verzeichnis des Plugins, mit der Umgebung aus dem nächsten Schritt.

  3. Lesen Sie main.js. Der Server übergibt dem Prozess drei Variablen: EVEY_PLUGIN_ID, EVEY_PLUGIN_SOCK (der Socket, auf dem Ihr Prozess lauschen und PluginService bedienen muss) und EVEY_HOST_SOCK (der Socket, mit dem Sie sich verbinden, um HostService aufzurufen). Das Beispiel lädt plugin_api.proto aus seinem eigenen Verzeichnis, wohin der Server die Datei kopiert hat, und setzt die fünf Methoden von PluginService um. Ändern Sie die Nachricht in Initialize:

    main.js (excerpt)
    Initialize(call, callback) {
      hostLog("LOG_LEVEL_INFO", `hello from ${process.env.EVEY_PLUGIN_ID}`);
      callback(null, { success: true });
    },
    
  4. Packen Sie es als ZIP. Das Manifest muss auf oberster Ebene des Archivs liegen, nicht in einem Unterverzeichnis:

    zip -r ../hello-plugin.zip . -x ".*"
    
  5. Installieren Sie es. Unter Verwaltung → Plugins wählen Sie Plugin installieren, dann das ZIP und Vorschau & Installieren: Die Vorschau listet api.library und das Ergebnis der statischen Analyse. Über die API sieht dasselbe Hochladen so aus:

    curl -F "plugin=@hello-plugin.zip" \
         -H "Authorization: Bearer $EVEY_TOKEN" \
         https://evey.test/api/v1/admin/plugins/upload
    
  6. Lesen Sie das Protokoll. Auf der Karte des Plugins zeigt Protokolle die Zeile, die Initialize geschrieben hat. docker compose logs evey enthält die Serverseite desselben Starts, unter der ID des Plugins.

  7. Arbeiten Sie weiter. Ändern Sie den Code, erhöhen Sie version, packen Sie neu und nutzen Sie Aus Quelle aktualisieren … auf der Karte. Ein Hochladen mit derselben oder einer niedrigeren Version wird abgelehnt. Eine neue Berechtigung im Manifest löst den Zustimmungsschritt aus, den der Administrator bestätigen muss.

Damit kennen Sie die gesamte Backend-Schnittstelle. Davon ausgehend lässt sich ein Plugin in drei Richtungen erweitern: den Server aufrufen (HostService, Abschnitt 4), eine Oberfläche darstellen (Abschnitt 5) und externe Dienste erreichen (Abschnitt 6).

4. Den Server aus dem Backend aufrufen

Jeder Aufruf an HostService trägt die Identität des Plugins: Der Server prüft den Prozess am Socket über seine PID, und ein Interceptor lehnt jeden Aufruf ab, dessen Berechtigung das Manifest nicht angibt. Drei Gruppen brauchen gar keine Berechtigung:

  • Log schreibt ins Protokoll des Plugins, das auf seiner Karte erscheint;
  • GetConfig liest die Werte, die der Administrator unter Konfiguration gesetzt hat (angegeben in config_schema); mit SetConfig legt das Plugin dort eigene Schlüssel ab;
  • GetData / SetData / DeleteData / ListDataKeys ist der private Schlüssel-Wert-Speicher des Plugins, der bei der Deinstallation gelöscht wird.

GetMediaItem und ListMediaItems brauchen api.library oder media.read; Fetch braucht network. Die vollständige Tabelle, mit den Methoden, die in der Schnittstelle definiert sind, aber noch nicht bedient werden, steht in der Plugin-API-Referenz.

5. Eine Oberfläche hinzufügen

Eine Oberfläche ist ein JavaScript-Modul, das die Seite in einem isolierten Frame lädt, in einem der Slots, die der Server erlaubt. Geben Sie an, wo die Dateien liegen und wohin das Modul kommt:

plugin.yaml
web_root: dist               # das Verzeichnis, das als statische Dateien ausgeliefert wird
frontend:
  - slot: home.section       # siehe die Liste der Slots
    src: dist/index.js       # muss unter web_root liegen

Das Modul bekommt eine Brücke window.evey: evey.onInit übergibt ihm die Konfiguration des Plugins, die Sprache, das Design und die Oberfläche, in der es angezeigt wird. evey.resize, evey.toast und evey.navigate wirken innerhalb fester Grenzen auf die Seite. Und wenn das Manifest prefs oder public angibt, erreichen evey.prefs.* und evey.media.* den Server über einen kurzlebigen Token, den der Host für den Frame verwaltet. examples/plugins/weather ist ein vollständiges Widget mit Build-Schritt; examples/plugins/data-demo zeigt prefs, public und den Slot admin.nav. Die Brücke und die Slots sind in der Plugin-API-Referenz beschrieben.

6. Externe Dienste erreichen

Ein Plugin-Prozess kann keinen Socket ins Internet öffnen, in allen Modi, in denen der Launcher läuft (basic ohne den Launcher verbietet nur execve). Die einzige Verbindung nach außen ist der Aufruf Fetch von HostService: Der Server prüft das Ziel gegen das Manifest und führt die Anfrage dann selbst aus.

plugin.yaml
permissions:
  - network
external_fetch:
  - api.open-meteo.com       # reine Hostnamen: kein Schema, kein Platzhalter, keine IP

network ohne external_fetch wird bei der Installation abgelehnt. Zur Laufzeit nimmt der Vermittler http und https, GET und HEAD an, liefert Antworten bis 8 MiB innerhalb von 20 Sekunden und lehnt private oder reservierte Adressen ab, auch über eine Weiterleitung. Der Frame der Oberfläche bekommt dieselbe Hostliste als connect-src. Ein Widget darf diese Hosts also direkt über https aufrufen. Die Regeln stehen in Berechtigungen und Slots.

Die Hostliste ist eine verbindliche Zusage an den Administrator

Der Administrator sieht die Hostliste beim Update und im Sicherheits-Dashboard und muss jede Ergänzung beim Update bestätigen. Beschränken Sie sie auf das, was das Plugin braucht, und schreiben Sie in Ihre README, was jeder Host bekommt.

7. Ihr Plugin signieren

Was eine Signatur beweist

Eine Signatur belegt, dass das ZIP, das der Administrator installiert, Byte für Byte das ist, das Sie veröffentlicht haben. Ihr öffentlicher Schlüssel reist im Paket (cosign.pub im Wurzelverzeichnis des ZIP), und Evey hält seinen Fingerabdruck bei der Installation am Plugin fest: Ein Update, das mit einem anderen Schlüssel signiert oder nicht mehr signiert ist, wird abgelehnt, bis der Administrator die Änderung akzeptiert. Sie sagt nichts darüber aus, was das Plugin tut: Berechtigungen, Hosts und die Analyse in der Vorschau gelten weiter. Evey prüft sie offline und im eigenen Prozess (ECDSA P-256 über den SHA-256 des ZIP, das Format, das cosign sign-blob --key erzeugt): kein cosign-Programm auf dem Server, kein Netz, kein Transparenzprotokoll.

Sie brauchen cosign nur auf Ihrem eigenen Rechner.

  1. Einmalig: ein Schlüsselpaar erzeugen.

    # eine echte Passphrase wählen; cosign.key bleibt privat, nie committen
    COSIGN_PASSWORD="your-passphrase" cosign generate-key-pair
    # -> cosign.key (privat) und cosign.pub (die Datei, die Sie weitergeben)
    
  2. Bei jeder Veröffentlichung: Ihren öffentlichen Schlüssel ins Wurzelverzeichnis des Plugins legen, packen, dann signieren, und zwar genau die Datei, die Sie verteilen.

    cp cosign.pub hello-plugin/cosign.pub
    cd hello-plugin && zip -r ../hello-plugin.zip . && cd ..
    
    COSIGN_PASSWORD="your-passphrase" cosign sign-blob \
      --key cosign.key --tlog-upload=false --yes \
      --output-signature hello-plugin.zip.sig \
      hello-plugin.zip
    
  3. Vor der Auslieferung prüfen, mit derselben Prüfung, die der Server macht:

    cosign verify-blob --key cosign.pub --signature hello-plugin.zip.sig \
      --insecure-ignore-tlog hello-plugin.zip      # -> Verified OK
    
  4. Die beiden Dateien zusammen veröffentlichen: hello-plugin.zip (Ihr öffentlicher Schlüssel steckt darin) und hello-plugin.zip.sig, nebeneinander, damit eine Installation per URL die Signatur unter <url>.sig findet. Schreiben Sie den Fingerabdruck des Schlüssels (sha256:..., von Evey auf der Karte des Plugins angezeigt) in Ihre README und Versionshinweise, damit Administratoren ihn vergleichen können, bevor sie dem Schlüssel vertrauen.

Auf der Instanz, die das Plugin installiert, wählt der Administrator das ZIP unter Verwaltung → Plugins und unter Signaturdatei (.zip.sig, optional) dessen .zip.sig. Über die API ist die Signatur das Feld sig beim Hochladen. Evey prüft das Paket gegen den Schlüssel, den es mitbringt, und zeigt eines von:

  • Signiert von : Der Schlüssel steht in der Liste der Vertrauenswürdigen Herausgeber der Instanz.
  • Signiert - unbekannter Schlüssel sha256:...: Die Signatur ist gut, dem Schlüssel wird noch nicht vertraut. Ein Klick, Diesem Schlüssel vertrauen, nimmt ihn unter einem Namen in die Liste auf.
  • Nicht signiert oder Ungültige Signatur.

Die Regel entscheidet, was geschieht: EVEY_PLUGINS_COSIGN_POLICY=warn (Standard) hält das Urteil fest und lässt jede Installation durch; enforce lässt nur vertraute Schlüssel zu. Unabhängig von der Regel wird ein Update, dessen Schlüssel von der installierten Version abweicht oder das die Signatur weglässt, abgelehnt, bis der Administrator im Update-Dialog Der Herausgeber hat gewechselt, ich akzeptiere den neuen Schlüssel ankreuzt. EVEY_PLUGINS_COSIGN_PUBLIC_KEY ist optional: Sie nimmt beim Start einen Schlüssel in die Vertrauensliste auf (als der des Serverbetreibers gekennzeichnet).

Typische Fehlerquellen:

  • Jede Änderung am ZIP macht die Signatur ungültig, sogar dieselben Dateien neu zu packen. Signieren Sie die Datei, die Sie verteilen, und signieren Sie jede Version neu.
  • Behalten Sie denselben Schlüssel von einer Version zur nächsten. Ein neuer Schlüssel ist für jeden Administrator, der Sie installiert hat, ein neuer Herausgeber: Er wird gebeten, ihn zu akzeptieren. Wechseln Sie mit Absicht und sagen Sie es in Ihren Versionshinweisen.
  • Nur ECDSA-P-256-Schlüssel werden angenommen, und genau die erzeugt cosign generate-key-pair. Schlüsselloses Signieren und Verweise auf ein KMS werden nicht geprüft.
  • Eine .sig ohne cosign.pub im ZIP, oder umgekehrt, gilt als Nicht signiert.
  • Ein falscher Schlüssel, ein manipuliertes ZIP und eine fehlende .sig gelten alle als fehlgeschlagene Signatur: Unter enforce wird die Installation oder das Update abgelehnt.

8. Was der Installer prüft, der Reihe nach

Jeder Schritt kann das Paket ablehnen. Der Administrator sieht den Grund, das Serverprotokoll enthält die Einzelheiten.

  1. Manifest. plugin.yaml wird direkt aus dem ZIP gelesen: Pflichtfelder, semver, bekannte Berechtigungen und Capabilities, Slots aus der erlaubten Liste, Einträge in external_fetch, die reine Hostnamen sind, web_root vorhanden, wenn frontend angegeben ist. Die angegebene language muss eine Laufzeit des Images sein.
  2. Version und Ausweitung. Bei einem schon installierten Plugin muss die neue Version höher sein, und jede neue Berechtigung, Capability, jeder neue Slot oder Host hält die Installation hier an, bis der Administrator den Unterschied bestätigt (accept_permission_changes=true auf der Upload-Route).
  3. Signatur und Hash. Die Signatur wird geprüft, wenn ein öffentlicher Schlüssel konfiguriert ist; ein Fehler blockiert nur unter enforce. Der Hash des ZIP wird festgehalten und erneut geprüft, wenn ein zuvor in der Vorschau gezeigtes Update angewendet wird.
  4. Entpacken in ein Zwischenverzeichnis: höchstens 5.000 Einträge, 100 MB pro Datei, 500 MB insgesamt, ein Kompressionsverhältnis von 100:1, kein symbolischer Link, kein Pfad, der das Archiv verlässt. Ein Update ändert die laufende Version erst, wenn alle folgenden Schritte bestanden sind.
  5. Nur Themes: Das Stylesheet wird bereinigt und zurückgeschrieben, und eine theme.js, die ein verbotenes Muster enthält, führt zur Ablehnung des Pakets.
  6. Statische Analyse jeder Textdatei: Ein kritischer Fund lehnt das Paket ab. Die Warnungen deserialization und obfuscation lehnen es ebenfalls ab, und native_code, kompilierte Binärdateien und eingebettete Archive lehnen es ab, außer das Manifest gibt native-binary an.
  7. Speichern und starten. Das Zwischenverzeichnis ersetzt das bisherige Verzeichnis, der Datensatz geschrieben und der Prozess gestartet.

Wonach die Analyse sucht, steht in Plugin-Sandbox und Bedrohungsmodell. Kommentare werden vor der Analyse entfernt, eine auskommentierte Zeile wird also nicht gemeldet.

9. Unter welchen Bedingungen Ihr Prozess läuft

Der Server startet den Prozess mit niedrigerer CPU-Priorität, einer Obergrenze für den Arbeitsspeicher, einem Limit für Prozesse und Threads und einer Größengrenze für sein Verzeichnis. Ein Wächter hält ein Plugin an, das sie überschreitet. Die Standardwerte sind 256 MiB, 64 Prozesse und 100 MiB. Der Administrator kann sie unter Verwaltung → Plugins → Ressourcenlimits anpassen. Ohne die Berechtigung subprocess kann der Prozess keine Kindprozesse starten, egal in welcher Sprache; Threads funktionieren.

In allen Modi, in denen der Launcher läuft, beschränkt die Sandbox das Dateisystem auf das Verzeichnis des Plugins und blockiert jeden Netzwerk-Socket (basic ohne den Launcher verbietet nur execve). Welcher Modus gilt, hängt vom Host ab, nicht vom Plugin: Plugin-Sandbox und Bedrohungsmodell beschreibt die Modi, und Plugin-Sandbox einrichten sagt einem Administrator, welchen er wählen soll.

10. Ein Plugin verteilen

Es gibt keinen Katalog: Ein Administrator installiert ein Plugin, indem er sein ZIP hochlädt oder seine URL im Verwaltungsbereich einfügt, egal wo Sie es veröffentlichen (ein Release auf einer Code-Plattform, Ihre eigene Website).

  • Liefern Sie das ZIP, seine .sig und Ihre cosign.pub zusammen aus (Abschnitt 7).
  • Nennen Sie die Berechtigungen und die Hosts aus external_fetch in Ihrer README: Der Administrator sieht die Berechtigungen bei der Installation und die Hosts beim Update und im Sicherheits-Dashboard und muss sie bestätigen. Ein unerwarteter Eintrag dort ist der häufigste Grund, warum eine Installation abgelehnt wird.
  • Damit Ihr Plugin bei den mitgelieferten Beispielen erscheint, öffnen Sie einen Pull Request gegen examples/plugins/ mit dem Quellcode und einer README. Beispiele werden geprüft und aus dem Quellcode gebaut; ein Eintrag ist keine Empfehlung und trägt keine Signatur des Projekts.

11. Fehlerbehebung

Symptom Ursache Lösung
plugin requires "nodejs" runtime which is not installed in this image Der Image-Variante fehlt der Interpreter. Nutzen Sie das Standard-Image oder -nodejs, oder schreiben Sie das Plugin in Python.
manifest: external_fetch entry "*.tmdb.org" is a wildcard - use explicit hostnames Platzhalter werden bei der Installation abgelehnt, ohne Ausnahme. Listen Sie jeden Hostnamen auf, den Sie brauchen (api.tmdb.org, image.tmdb.org).
manifest: frontend slot "admin.section" not allowed Der Slot steht nicht in der erlaubten Liste. Wählen Sie einen Slot aus der Liste der Slots; admin.nav ist der einzige Slot im Verwaltungsbereich.
manifest: network permission requires non-empty external_fetch network ohne Hosts angegeben. Ergänzen Sie die Hosts oder streichen Sie die Berechtigung.
plugin already installed at version 1.0.0, new version must be higher Das ZIP trägt die installierte Version. Erhöhen Sie version oder deinstallieren Sie zuerst.
signature does not match the plugin or the configured key Die .sig wurde mit einem anderen Schlüssel erstellt, oder das ZIP hat sich nach dem Signieren geändert. Signieren Sie genau dieses ZIP neu mit dem richtigen Schlüssel, oder korrigieren Sie EVEY_PLUGINS_COSIGN_PUBLIC_KEY.
plugin blocked: theme.js contains forbidden pattern Der Scanner für Theme-Skripte hat eine verbotene API gefunden. Ein Theme darf nur evey.theme.* aufrufen: siehe Themes.
Das Protokoll endet mit memory quota exceeded (... MB > ... MB) Der Wächter hat das Plugin über seinem Speicherlimit angehalten. Senken Sie den Speicherverbrauch oder erhöhen Sie Max. Arbeitsspeicher (MiB) unter Ressourcenlimits.
bwrap: ... permission denied Modus bubblewrap in Docker, blockiert vom seccomp- oder AppArmor-Profil des Containers. Behalten Sie den Standardmodus auto. Sonst siehe Plugin-Sandbox einrichten.
Das Oberflächenmodul erscheint nicht Das Plugin ist deaktiviert, der Slot falsch geschrieben, oder src liegt nicht unter web_root. Prüfen Sie den Zustand der Karte, den Namen des Slots und die Anfragen an /api/v1/plugins/<id>/assets/... im Netzwerkbereich des Browsers.