Plugin-Entwicklung für Iris
  • Python 70.6%
  • HTML 17.1%
  • Shell 7.4%
  • Swift 2.3%
  • JavaScript 2.2%
  • Other 0.4%
Find a file
2026-11-12 21:59:02 +01:00
_shared feat(web): Plugin-Seiten auf Theme-Variablen --iris-* umgestellt (#325) 2026-10-31 14:16:36 +01:00
blender-asset-generator chore: alle Plugins um eine Micro-Version erhöht 2026-09-12 23:03:43 +02:00
blender-bridge chore: alle Plugins um eine Micro-Version erhöht 2026-09-12 23:03:43 +02:00
blender-comfyui-pipeline chore: alle Plugins um eine Micro-Version erhöht 2026-09-12 23:03:43 +02:00
blender-remesh feat(blender-remesh): initial commit 2026-08-03 22:37:25 +02:00
comfyui-bridge fix(comfyui-bridge): Read-Timeout bei /history gilt nicht mehr als Fehler (iris/iris-plugins#349) 2026-11-05 19:57:32 +01:00
comfyui-dirigent docs(plugins): Anleitung 'Erste Schritte' für 15 Plugins (docs/, user_docs, Build packt sie), changelog-Einträge; iris-plugin-dev 0.1.10: pruefen prüft user_docs (#194) 2026-10-21 23:30:33 +02:00
comfyui-workflows feat(comfyui-workflows): schmale Ansicht zeigt Liste oder Workflow (iris/iris-plugins#341) 2026-11-04 17:30:07 +01:00
email chore(plugins): Versionen und Änderungstexte für die Hell/Dunkel-Seiten (#325) 2026-10-31 14:24:31 +01:00
file-picker chore(plugins): Versionen und Änderungstexte für die Hell/Dunkel-Seiten (#325) 2026-10-31 14:24:31 +01:00
filmcraft chore(plugins): Versionen und Änderungstexte für die Hell/Dunkel-Seiten (#325) 2026-10-31 14:24:31 +01:00
freecad-bridge feat(web): Plugin-Seiten auf Theme-Variablen --iris-* umgestellt (#325) 2026-10-31 14:16:36 +01:00
heartmula chore(plugins): Versionen und Änderungstexte für die Hell/Dunkel-Seiten (#325) 2026-10-31 14:24:31 +01:00
ios-install fix(ios-install): list-devices verträgt Simulatoren ohne Entwicklermodus und Tunnel-Zustand (iris/iris-plugins#123) 2026-10-19 18:40:26 +02:00
iris-browser fix(iris-browser): Update startet Chromium mit alten Startoptionen neu — 0.5.5 (iris/iris-plugins#355) 2026-11-05 22:09:46 +01:00
iris-buchhaltung feat(web): Plugin-Seiten auf Theme-Variablen --iris-* umgestellt (#325) 2026-10-31 14:16:36 +01:00
iris-claude-sessions fix(claude-sessions): Fortsetzen legt ein fehlendes Arbeitsverzeichnis an (0.15.3) (iris/iris-plugins#343) 2026-11-05 20:36:01 +01:00
iris-dev-documentation chore: alle Plugins um eine Micro-Version erhöht 2026-09-12 23:03:43 +02:00
iris-dosgame docs(plugins): store/changelog.md für 22 Plugins aus dem Verlauf der veröffentlichten Versionen (iris-plugins#186) 2026-10-21 22:58:20 +02:00
iris-editable-table chore(plugins): Versionen und Änderungstexte für die Hell/Dunkel-Seiten (#325) 2026-10-31 14:24:31 +01:00
iris-forgejo docs(iris-forgejo): LAN-Installation, zwei Instanzen, http im LAN prüfen (#113) 2026-10-17 01:13:14 +02:00
iris-gamebox chore(plugins): Versionen und Änderungstexte für die Hell/Dunkel-Seiten (#325) 2026-10-31 14:24:31 +01:00
iris-ghidra fix(iris-ghidra): LE/LX-Loader für Ghidra 12.2, gepinnt und mit Korrektur (0.2.2) (#367) 2026-11-06 13:28:34 +01:00
iris-gui docs(plugins): store/changelog.md für 22 Plugins aus dem Verlauf der veröffentlichten Versionen (iris-plugins#186) 2026-10-21 22:58:20 +02:00
iris-jobs chore(plugins): Versionen und Änderungstexte für die Hell/Dunkel-Seiten (#325) 2026-10-31 14:24:31 +01:00
iris-nextcloud-cookbook chore(plugins): Anleitungs-Versionen claude-sessions 0.12.1 und cookbook 0.14.1 2026-11-01 10:34:57 +01:00
iris-pandoc docs(plugins): Anleitung 'Erste Schritte' für 15 Plugins (docs/, user_docs, Build packt sie), changelog-Einträge; iris-plugin-dev 0.1.10: pruefen prüft user_docs (#194) 2026-10-21 23:30:33 +02:00
iris-plugin-dev chore(plugins): Versionen und Änderungstexte für die Hell/Dunkel-Seiten (#325) 2026-10-31 14:24:31 +01:00
iris-rea-recherche feat(iris-rea-recherche): Entscheidungen des Nutzers im Speicher, Kontextpaket und kontext.json (iris/iris-plugins#385) 2026-11-06 21:07:35 +01:00
iris-repo-check fix(iris-repo-check): Bericht und Aufräumen vor der Schlussantwort, v0.1.1 (#74) 2026-10-16 17:56:40 +02:00
iris-rustapp-crafter fix(iris-rustapp-crafter): Neustart trifft das Gateway, nicht die Weboberfläche (0.4.1) (#388) 2026-11-12 21:59:02 +01:00
iris-server-hosts chore(plugins): Versionen und Änderungstexte für die Hell/Dunkel-Seiten (#325) 2026-10-31 14:24:31 +01:00
iris-solidcraft chore(plugins): Versionen und Änderungstexte für die Hell/Dunkel-Seiten (#325) 2026-10-31 14:24:31 +01:00
iris-starter-test feat(starter-test): Test-Plugin für autonome Web-Blöcke — Starter, Aktionen ohne Modell-Lauf, Zustand (iris/Iris#149) 2026-10-24 12:46:14 +02:00
iris-web-automation chore(plugins): Versionen und Änderungstexte für die Hell/Dunkel-Seiten (#325) 2026-10-31 14:24:31 +01:00
marker chore(plugins): Versionen und Änderungstexte für die Hell/Dunkel-Seiten (#325) 2026-10-31 14:24:31 +01:00
marker-project fix(marker): Leerlauf-Entladen über einen Arbeitsprozess (Speicher ließ sich im Prozess nicht freigeben) 2026-10-09 10:13:26 +02:00
mask-editor-reference feat(web): Plugin-Seiten auf Theme-Variablen --iris-* umgestellt (#325) 2026-10-31 14:16:36 +01:00
model-convert/scripts feat(wordcraft): Plugin — TCP-Brücke zu WordCraft, 11 Befehle, README + CLAUDE.md 2026-10-10 16:57:05 +02:00
openmontage feat(web): Plugin-Seiten auf Theme-Variablen --iris-* umgestellt (#325) 2026-10-31 14:16:36 +01:00
pdfcraft docs(pdfcraft): PLAN auf E2E-Endstand (Mac ✅, Linux blockiert), 6 neue Lessons 2026-10-10 21:45:26 +02:00
plugin-marktplatz docs(plugins): Registry-Katalog — Vorschläge vom Forgejo bei Quelle hinzufügen (iris/Iris#69) 2026-10-28 15:27:35 +01:00
qwen-tts-linux chore(plugins): Versionen und Änderungstexte für die Hell/Dunkel-Seiten (#325) 2026-10-31 14:24:31 +01:00
qwen-tts-mac docs(plugins): Anleitung 'Erste Schritte' für 15 Plugins (docs/, user_docs, Build packt sie), changelog-Einträge; iris-plugin-dev 0.1.10: pruefen prüft user_docs (#194) 2026-10-21 23:30:33 +02:00
registry-tools feat(iris-claude-sessions): Suchquelle für den Skill memory — Sessions und Claude-Memory (0.10.0, Paket 1; iris/iris-plugins#305) 2026-10-27 01:49:24 +01:00
strata-modelle feat(iris-jobs): Reiter Hintergrundjobs mit Abbrechen; Reporter schreibt Job-Register; translate cancel (1.0.7); marker, heartmula, strata-modelle, comfyui-bridge neu gebündelt (#287) 2026-10-26 16:59:01 +01:00
teach chore: alle Plugins um eine Micro-Version erhöht 2026-09-12 23:03:43 +02:00
translate feat(iris-jobs): Reiter Hintergrundjobs mit Abbrechen; Reporter schreibt Job-Register; translate cancel (1.0.7); marker, heartmula, strata-modelle, comfyui-bridge neu gebündelt (#287) 2026-10-26 16:59:01 +01:00
translation-matrix feat(web): Plugin-Seiten auf Theme-Variablen --iris-* umgestellt (#325) 2026-10-31 14:16:36 +01:00
whisper chore(plugins): Versionen und Änderungstexte für die Hell/Dunkel-Seiten (#325) 2026-10-31 14:24:31 +01:00
wordcraft docs(claude-md): filmcraft und wordcraft auf den Vertrag abgestimmt — ship statt direktem Commit, falschen Registry-Verweis durch Verweis ersetzt, Offenes in den Plan 2026-10-12 14:15:28 +02:00
.gitignore feat(browser): Menschliches Benutzerverhalten: Rhythmus, Lese-Pausen, --fast, --eval in Hauptwelt; 0.5.0 (#202) 2026-10-22 15:55:07 +02:00
build-all-plugins.sh feat(file-picker): neues Canvas-Block-Plugin für Dateiauswahl 2026-08-13 11:43:02 +02:00
CLAUDE.md feat(iris-gamebox): Ansichtszustand — Iris setzt Suche und zeigt den ersten Treffer, schaltet durch die Treffer; ViewState im Panel-Kit, Version 0.10.0 (#284) 2026-10-26 15:42:35 +01:00
PLUGIN-REGISTRY-REPO.md feat(plugins): Action-Contract tooling — Manifest-Prüfung, Lock-Werkzeug, Privacy-Scan, Store-Seite (iris-plugins#225) 2026-10-23 00:05:45 +02:00
publish-plugins.sh feat(registry): Anleitung aus docs/ (user_docs) als store.docs und anleitung/*.html, publish-plugins.sh --docs, README (#194) 2026-10-21 23:23:17 +02:00
README.md feat(iris-browser): Tab-Liste gelangt mit dem geteilten Chat auf den anderen Rechner, 0.5.3 (iris/iris-plugins#347) 2026-11-05 16:45:30 +01:00
session-kontext.md feat(iris-rustapp-crafter): Figcraft aus der Paket-Registry installieren, CLI-Suche (0.2.0) (#388) 2026-11-12 19:51:51 +01:00

Iris Plugin-System — Entwicklerdokumentation

Iris-Plugins bündeln Hermes-Skills, Python-Abhängigkeiten, Lifecycle-Hooks, Systemprompts und Prompt-Templates in einem einzelnen .irisplugin-Paket. Das Paket wird in der Iris-GUI importiert und vollautomatisch installiert.

Stand 2026-09-13: Plugin-System vollständig implementiert — Import-GUI, Inline-Installationslog, Install-Pipeline (venv, Symlinks, System-Prompt, Templates, Hooks), optionale Komponenten, Deinstallation, plus das seit 2026-08-18 gebaute Web-View-System für Settings-Panels (kein natives .framework mehr nötig), der seit 2026-09-12 getrennte Konfigurieren-/ Anwenden-Direktzugriff (Plugins-Chevron) und die seit 2026-09-13 pluralen scheduled_job_templates (Abschnitt 17) — ein Plugin kann mehrere Hintergrund-Job-Vorlagen als "System-Vorlagen"-Karten anbieten, statt bei Install still einen Job anzulegen. Referenz-Plugin: iris-web-automation (nutzt Skills, venv, Hooks, Settings-Web-View und Plugins-Chevron gemeinsam).

Stand 2026-10-16: Plugin-Marktplatz. Plugins lassen sich in Iris wie in einem App Store nach Kategorien durchsuchen, mit Icon, Store-Seite und eigener Notiz (Bilder, Videos) ansehen und mit einem Klick installieren, aktualisieren (auch alle auf einmal) und entfernen — vorher zeigt ein Dialog, was das Paket tut (Skripte, Skills, Pakete, jede Datei zum Ansehen). Was ein Plugin dafür mitbringen muss (category, requires, platforms, Ordner store/ mit custom.md), steht in Kapitel 20.

Stand 2026-10-16: Plugins entwickeln. Das Plugin iris-plugin-dev führt ein neues Plugin per State Machine von der Idee (grill-me, Plan, Gerüst, Repo, Prüfung, Store-Material) bis zur Veröffentlichung; Plan und Entscheidungen: iris-plugin-dev/PLAN.md. Seine Entwickler-Doku (docs/) wird beim Bauen aus den Kapiteln 2 bis 5, 7, 8, 10, 11, 13, 14, 15, 19 und 20 dieser README erzeugt: Diese README bleibt die einzige Quelle, Kapitelnummern also nicht ohne Blick auf iris-plugin-dev/skills/iris-plugin-dev/pd_kern.py (DOKU_THEMEN) ändern.


Inhaltsverzeichnis

  1. Konzept
  2. Paketformat: .irisplugin
  3. plugin.json — Manifest-Referenz
  4. Lifecycle Hooks
  5. Verzeichnisstruktur im Paket
  6. Installierter Zustand (Application Support)
  7. Entwicklungsworkflow
  8. build-irisplugin.sh
  9. Installations-Ablauf in Iris
  10. Skill-Verknüpfung mit Hermes und Claude Code
  11. System-Prompt und Templates
  12. Optionale Komponenten
  13. Versionierung und Updates
  14. Konventionen und Einschränkungen
  15. Settings-Panel & Direktzugriff (Web-Views)
  16. Referenz-Plugin: Web-Automation
  17. Scheduled-Job-Vorlagen (System-Vorlagen)
  18. Veröffentlichung im offiziellen Repository (Plugin-Registry) — früherer Entwurf, ersetzt durch Kapitel 19
  19. Plugins veröffentlichen: publish-plugins.sh und das Registry-Repo
  20. Marktplatz und Store: Plugins store-tauglich machen
  21. Rechner, Rechnerwahl und Konfigurations-Zustand (host_capabilities, local_mode, config-status)
  22. Seitenpanel und Chat: wie ein Plugin mit dem Chat spricht (side_panel, status, select, panel_kit)

1. Konzept

Ein Plugin kapselt alles, was ein Iris-Agent braucht, um eine neue Fähigkeit zu bekommen:

Komponente Zweck
Skill-Verzeichnis SKILL.md — lehrt Hermes wie das Plugin zu bedienen ist
Python-Core die eigentliche Logik (Tools, Pipelines, Bibliotheken)
Python-venv isolierte Laufzeitumgebung, pro Plugin, in Application Support
Lifecycle Hooks Shell-Skripte für pre_install, post_install, pre_uninstall
System-Prompt vorkonfigurierter Iris-Agenten-Prompt für diesen Anwendungsfall
Templates wiederverwendbare Prompt-Vorlagen, automatisch in Dreamer registriert

Grundregel: Alles landet in Application Support.

~/Library/Application Support/Iris/plugins/<plugin-id>/

Kein Plugin schreibt direkt nach ~/.hermes/, ~/.claude/ oder andere globale Verzeichnisse. Iris richtet die nötigen Symlinks nach der Installation selbst ein.


2. Paketformat: .irisplugin

Ein .irisplugin ist ein gewöhnliches ZIP-Archiv mit der Dateiendung .irisplugin. Die Struktur im Archiv:

plugin.json                     ← Pflicht: Manifest
skills/
  <skill-name>/
    SKILL.md                    ← Pflicht: mindestens ein Skill
    [weitere Skill-Dateien]
core/                           ← Optional: Python-Logik (Tools, Pipelines, …)
  [beliebige Verzeichnisstruktur]
hooks/                          ← Optional: Lifecycle-Skripte
  pre_install.sh
  post_install.sh
  pre_uninstall.sh
python/
  requirements.txt              ← Pflicht wenn python.venv=true
  requirements-<key>.txt        ← Optional: optionale Komponenten
system_prompt.md                ← Optional: Iris-Systemprompt
templates/
  *.md                          ← Optional: Dreamer-Templates

Was kommt NICHT ins Paket

  • .venv/, venv/ — werden bei der Installation neu angelegt
  • __pycache__/, *.pyc — Build-Artefakte
  • .env, .env.local — niemals Secrets ins Paket
  • .git/ — Versionierungsdaten
  • node_modules/ — werden vom post_install-Hook neu installiert
  • Test-Daten, Beispiel-Renders, große Mediendateien, ML-Modell-Gewichte
  • store/ — das Material für den Marktplatz (Icon, Store-Seite, Langtext, eigene Notiz mit Bildern, Kapitel 20) liegt neben dem Paket in der Registry; publish-plugins.sh lehnt ein Paket ab, das store/ enthält

3. plugin.json — Manifest-Referenz

{
  "id": "openmontage",
  "name": "OpenMontage",
  "version": "1.0.0",
  "description": "KI-gestützte Videoproduktion.",
  "author": "alf",
  "iris_min_version": "2.0.0",

  "skills": ["openmontage"],

  "system_prompt": true,
  "system_prompt_name": "OpenMontage",
  "system_prompt_usage": "Aufrufen, wenn ein Video mit OpenMontage, Hyperframes oder Remotion erzeugt werden soll.",

  "templates": [
    "templates/neues-video.md",
    "templates/video-aus-clips.md"
  ],

  "python": {
    "venv": true,
    "requirements": "python/requirements.txt",
    "optional": {
      "ai_music": "python/requirements-ai_music.txt"
    }
  },

  "hooks": {
    "pre_install":   "hooks/pre_install.sh",
    "post_install":  "hooks/post_install.sh",
    "pre_uninstall": "hooks/pre_uninstall.sh"
  }
}

Feldbeschreibungen

Feld Typ Pflicht Beschreibung
id string ✅ Eindeutige Kennung, kebab-case. Wird als Verzeichnisname in Application Support verwendet. Ändert sich nie.
name string ✅ Anzeigename in der Iris-GUI.
version string ✅ SemVer (1.0.0). Iris vergleicht beim Update.
description string ✅ Einzeiliger Beschreibungstext.
author string — Name oder Handle des Autors.
iris_min_version string — Mindest-Iris-Version. Iris warnt bei Unterschreitung (blockiert nicht).
category string ✅ zum Veröffentlichen Kategorie im Marktplatz: eine ID aus registry-tools/categories.json (media, knowledge, automation, development, infrastructure, security). Genau eine. publish-plugins.sh lehnt ein Plugin ohne oder mit unbekannter Kategorie ab.
changelog string — Was sich in dieser Version geändert hat, in ein bis drei Sätzen (höchstens 1000 Zeichen), für Anwender geschrieben. Der Update-Dialog des Marktplatzes zeigt den Text statt der geänderten Dateien, und die Versionsliste zeigt ihn zu jeder Version. Bei jeder neuen Version ersetzen, nicht ergänzen. Fehlt das Feld, zeigt der Dialog die geänderten Dateien.
user_docs object[] — Die Anleitung für Anwender: Seiten {"title": "Erste Schritte", "path": "erste-schritte.md"} aus dem Ordner docs/ im Paket (Markdown, kein Bild, kein Link). Der Plugin-Manager zeigt sie; publish-plugins.sh veröffentlicht sie zusätzlich als Anleitung im Marktplatz (Symbol „Anleitung“ in der Kachel), ohne neue Version, wenn nur der Text der Seiten sich ändert. Die Datei muss im Paket liegen: Das Build-Skript kopiert docs/*.md. Siehe Kapitel 20.
requires string[] — IDs von Plugins, die dieses Plugin braucht (zum Beispiel ["comfyui-bridge"]). Der Marktplatz zeigt sie und installiert sie mit. Jede ID muss in der Registry stehen.
disables_hermes_toolsets string[] — Eingebaute Hermes-Toolsets (Namen wie in hermes tools list, zum Beispiel ["browser"]), die dieses Plugin ablöst. Solange das Plugin installiert und aktiviert ist, sind sie für den Chat (Plattform api_server) abgeschaltet, sonst nimmt das Modell das fertige Werkzeug von Hermes statt des Plugin-Skills. Beide Server gleichen das selbst ab, bei Installieren, Aktivieren/Deaktivieren und Entfernen und beim Start: Mac über die Hermes-CLI (HermesToolsetPolicy.swift; schaltet nur ab, was der Nutzer nicht schon selbst abgeschaltet hatte, und schaltet beim Entfernen wieder ein), Linux über den Admin-Block der Hermes-Konfiguration (hermes_config_writer.plugin_disabled_toolsets, hermes_toolsets.sync). Hermes wird nur bei einer echten Änderung neu gestartet. Ein Plugin braucht dafür keinen Hook und keine Änderung an der Hermes-Konfiguration.
platforms string[] — Wo das Plugin läuft: ["macos"], ["linux"] oder beides. Ohne Angabe gilt: mit native_framework_path nur Mac, sonst beides. Angeben, wenn es nur auf einer Seite sinnvoll ist (zum Beispiel ein MLX-Treiber, der Apple Silicon braucht: ["macos"]), sonst bietet der Marktplatz es auch dort zum Laden an. Plugins, die einen entfernten Rechner nutzen (Whisper, HeartMuLa, Qwen TTS Linux/CUDA), laufen auf beiden Seiten und brauchen keine Angabe; den Rechner wählt der Nutzer aus der Liste des Plugins iris-server-hosts (Kapitel 21). Auf dem falschen Server erscheint das Plugin gesperrt mit Grund.
skills string[] ✅ Namen der Skill-Verzeichnisse unter skills/ im Paket.
system_prompt bool — true wenn system_prompt.md im Paket vorhanden ist.
system_prompt_name string — Anzeigename des Prompts im System-Prompt-Picker (z.B. "OpenMontage"). Standard: name.
system_prompt_usage string — Verwendungstext im System-Prompt-Picker. Sollte beschreiben wann der Categorizer dieses Prompt aktivieren soll. Standard: "<name> Plugin-Prompt".
templates string[] — Relative Pfade zu Template-Dateien. Werden in dreamer-templates/ als plugin-<id>-<basename>.md registriert.
python.venv bool — true = Iris legt bei Installation ein venv an.
python.requirements string — Relativer Pfad zur requirements.txt.
python.optional object — Schlüssel → Pfad-Map für optionale Requirements-Dateien.
hooks.pre_install string — Skript läuft aus dem Staging-Verzeichnis nach Manifest-Validierung.
hooks.post_install string — Skript läuft aus dem installierten Plugin-Verzeichnis nach allen Registrierungen.
hooks.pre_uninstall string — Skript läuft vor dem ersten Removal-Schritt. Fehler blockiert Deinstallation nicht.
has_settings_panel bool — true wenn das Plugin ein Konfigurations-Panel hat — nativ (native_framework_path, veraltet, siehe unten) oder als Web-View (settings_web_view_path). Siehe Abschnitt 15.
settings_web_view_path string — Pfad zur HTML-Datei für das "Konfigurieren"-Panel (im Plugin-Manager). Siehe Abschnitt 15.
usage_web_view_path string — Pfad zur HTML-Datei für den "Anwenden"-Direktzugriff (Plugins-Chevron). Siehe Abschnitt 15.
driver_group string — Markiert dieses Plugin als austauschbaren Treiber innerhalb einer Gruppe (z.B. qwen-tts) — Installation eines anderen Mitglieds derselben Gruppe deinstalliert automatisch das vorherige. Siehe Abschnitt 15.
quick_access_label string — Menütext im Plugins-Chevron (z.B. "Logindaten und Web-Automatisierung"). Fehlt das Feld, erscheint das Plugin dort gar nicht. Siehe Abschnitt 15.
quick_access_icon string — SF-Symbol-Name für den Chevron-Eintrag (z.B. "key.fill"). Fällt ohne Angabe auf ein generisches Icon zurück.
panel_api object — Standardweg für das Backend eines Settings-/Anwenden-Panels. {"script": "skills/<name>/panel_api.py", "commands": ["status", "save"], "timeout": 60} — Iris führt über die generische Route POST /plugins/<id>/api/<befehl> genau dieses Skript aus. Ein Plugin mit Konfigurationsfenster muss den Befehl config-status führen (Zustand für den Marktplatz, Kapitel 21); publish-plugins.sh lehnt es sonst ab. Siehe Abschnitt 15, "Panel-Backend ohne Iris-Änderung".
host_capabilities string[] — Was ein Rechner mitbringen muss, auf dem das Plugin läuft: gpu (NVIDIA-Karte erkannt), docker, os. Die Installations-Sperre lässt das Plugin nur zu, wenn mindestens ein Rechner alle Anforderungen erfüllt, und der Host-Picker zeigt nur passende Rechner. Fehlt das Feld, verlangt das Plugin nichts; ein Wert, der keine Liste von Namen ist (Tippfehler), gewinnt nie. Siehe Kapitel 21.
local_mode bool | string[] — Das Plugin läuft auch lokal auf dem Iris-Server: true (überall) oder eine Plattformliste (["macos"], ["linux"]). Die Installations-Sperre blockiert ein Plugin mit lokalem Weg dort nie, und der Picker führt „Dieser Iris-Server (lokal)“ zusätzlich. Siehe Kapitel 21.
scheduled_job_templates array — Ein oder mehrere Hintergrund-Job-Vorlagen, die als "System-Vorlagen"-Karten im Hintergrund-Jobs-Panel erscheinen. Siehe Abschnitt 17.

4. Lifecycle Hooks

Hooks sind Shell-Skripte die Iris an definierten Punkten des Install- oder Deinstall-Ablaufs ausführt.

Umgebungsvariablen (automatisch gesetzt)

Variable Inhalt
IRIS_PLUGIN_DIR Absoluter Pfad zum installierten Plugin-Verzeichnis
IRIS_PLUGIN_ID Plugin-ID aus plugin.json
IRIS_PLUGIN_VENV Absoluter Pfad zum .venv-Verzeichnis des Plugins
IRIS_APP_SUPPORT ~/Library/Application Support/Iris/
IRIS_HOST_MODE Nur bei Plugins mit host_capabilities und nur auf dem Mac-Server (der Linux-Server führt keine Hooks aus): local — kein passender Rechner, das Plugin hat einen lokalen Weg, der Hook richtet ihn ein; pending — mindestens ein passender Rechner, der Hook richtet nichts ein (der Nutzer wählt im Konfigurationsfenster). Fehlt bei Plugins ohne Rechner-Anforderung. Siehe Kapitel 21.
PATH Erweitert um $IRIS_PLUGIN_VENV/bin und /opt/homebrew/bin — pip, node, npm direkt verwendbar ohne Venv-Aktivierung

Exit-Codes

  • 0 = Erfolg
  • ≠ 0 = Fehler; bei pre_install und post_install wird die Installation abgebrochen und der Fehler in der Plugin-Karte angezeigt; bei pre_uninstall wird der Fehler geloggt, die Deinstallation läuft aber weiter (User will das Plugin weg)

Hook-Timing

Import einer .irisplugin-Datei:
  1. Manifest validieren (plugin.json lesen + prüfen)
  2. ← pre_install.sh (aus Staging-Dir)
  3. Plugin-Verzeichnis anlegen / altes sichern
  4. Archiv entpacken
  5. Python venv einrichten + Requirements installieren
  6. Skill-Symlinks anlegen
  7. System-Prompt registrieren
  8. Dreamer-Templates registrieren
  9. ← post_install.sh (aus Plugin-Dir)
 10. Index speichern

Deinstallation:
  1. ← pre_uninstall.sh (aus Plugin-Dir, Plugin noch vollständig vorhanden)
  2. Symlinks entfernen
  3. System-Prompt entfernen
  4. Dreamer-Templates entfernen
  5. Plugin-Verzeichnis in Papierkorb
  6. Index aktualisieren

Typische Anwendungsfälle

pre_install.sh — Systemvoraussetzungen prüfen:

#!/usr/bin/env bash
set -euo pipefail
command -v ffmpeg >/dev/null || { echo "FFmpeg fehlt. brew install ffmpeg"; exit 1; }
command -v node   >/dev/null || { echo "Node.js fehlt. brew install node"; exit 1; }

post_install.sh — Node.js-Abhängigkeiten installieren:

#!/usr/bin/env bash
set -euo pipefail
COMPOSER_DIR="$IRIS_PLUGIN_DIR/core/remotion-composer"
[ -d "$COMPOSER_DIR/node_modules" ] && { echo "node_modules bereits vorhanden."; exit 0; }
cd "$COMPOSER_DIR"
npm install --prefer-offline 2>&1 | sed 's/^/[npm] /'

pre_uninstall.sh — Nutzerdaten sichern:

#!/usr/bin/env bash
WORKSPACE_DIR="$HOME/Documents/openmontage-projects"
[ -d "$WORKSPACE_DIR" ] && echo "Hinweis: $WORKSPACE_DIR bleibt erhalten." || true

5. Verzeichnisstruktur im Paket

Entwicklungsstruktur (Quell-Repository)

plugins/
  <plugin-id>/
    build-irisplugin.sh     ← Deployment-Skript
    plugin.json             ← Manifest
    hooks/
      pre_install.sh        ← optional
      post_install.sh       ← optional
      pre_uninstall.sh      ← optional
    skills/
      <skill-name>/
        SKILL.md
        [weitere Dateien]
    python/
      requirements.txt
      requirements-<key>.txt   ← optionale Komponenten
    system_prompt.md
    templates/
      aufgabe-a.md
      aufgabe-b.md
    store/                  ← optional, NICHT im Paket: Marktplatz-Material (Kapitel 20)
      icon.svg
      description.md
      custom.md             ← eigene Notiz (Obsidian-Stil), Bilder/Videos in _attachments/
      changelog.md          ← vollständige Changelog (ein Abschnitt je Version)
      _attachments/uebersicht.png

Die eigentliche Plugin-Logik (Tools, Pipelines) lebt im Projekt-Verzeichnis (z.B. ~/openmontage-project/). Das Build-Skript kopiert sie als core/ ins Archiv.

Im Archiv nach dem Build (openmontage als Beispiel)

plugin.json
hooks/
  post_install.sh
skills/
  openmontage/
    SKILL.md
core/
  tools/
  pipeline_defs/
  lib/
  skills/
  schemas/
  styles/
  remotion-composer/       ← Quelle ohne node_modules (~1,6 MB)
    package.json
    src/
    ...
  config.yaml
python/
  requirements.txt
  requirements-ai_music.txt
system_prompt.md
templates/
  neues-video.md
  video-aus-clips.md

6. Installierter Zustand (Application Support)

Nach dem Import in Iris:

~/Library/Application Support/Iris/plugins/
  openmontage/
    plugin.json
    hooks/
      post_install.sh
    core/
      tools/
      pipeline_defs/
      lib/
      skills/
      schemas/
      styles/
      remotion-composer/
        node_modules/        ← von post_install.sh via npm install angelegt
        package.json
        src/
    .venv/                   ← von Iris angelegtes Python-venv (Homebrew Python 3.13+)
      bin/
        python3
        pip
        piper                ← wenn piper-tts in requirements.txt
      lib/python3.x/site-packages/
    skills/
      openmontage/
        SKILL.md
    system_prompt.md
    templates/
      neues-video.md
      video-aus-clips.md
    python/
      requirements.txt
      requirements-ai_music.txt

~/Library/Application Support/Iris/dreamer-templates/
  plugin-openmontage-neues-video.md      ← kopiert von Iris bei Installation
  plugin-openmontage-video-aus-clips.md
~/.hermes/skills/openmontage  →  AppSupport/Iris/plugins/openmontage/skills/openmontage
~/.claude/skills/openmontage  →  AppSupport/Iris/plugins/openmontage/skills/openmontage

Python-Executable-Auswahl

Iris bevorzugt Homebrew Python in absteigender Versionreihenfolge:

/opt/homebrew/bin/python3.13 → 3.12 → 3.11 → 3.10 → /opt/homebrew/bin/python3
/usr/local/bin/python3.13 → …
/usr/bin/python3  ← letzter Fallback (macOS System-Python, aktuell 3.9 — EOL)

Das System-Python wird bewusst als Fallback behandelt, weil moderne Pakete (onnxruntime, scipy 1.10+) keine Python-3.9-Wheels mehr liefern.


7. Entwicklungsworkflow

Neues Plugin anlegen

mkdir -p ~/Documents/2ndbrain/plugins/<mein-plugin>
cd ~/Documents/2ndbrain/plugins/<mein-plugin>
mkdir -p skills/<skill-name> python templates hooks
touch plugin.json skills/<skill-name>/SKILL.md system_prompt.md
touch python/requirements.txt

plugin.json Minimal-Template:

{
  "id": "mein-plugin",
  "name": "Mein Plugin",
  "version": "1.0.0",
  "description": "Einzeiler.",
  "skills": ["mein-plugin"],
  "python": { "venv": true, "requirements": "python/requirements.txt" }
}

SYMLINK_CORE=true im Build-Skript bewirkt, dass Iris core/ als Symlink auf das Quell-Verzeichnis installiert statt einer Kopie — Änderungen wirken sofort ohne Neu-Build.

# In build-irisplugin.sh:
SYMLINK_CORE=true

Nur für den eigenen Rechner. Im Produktions-Build immer SYMLINK_CORE=false.

Paket bauen und testen

bash ~/Documents/2ndbrain/plugins/openmontage/build-irisplugin.sh
# → ~/Desktop/openmontage-1.0.0.irisplugin

# Inhalt prüfen:
unzip -l ~/Desktop/openmontage-1.0.0.irisplugin | head -30

8. build-irisplugin.sh

Jedes Plugin hat eine eigene Kopie des Build-Skripts im Plugin-Verzeichnis. Der Konfigurationsblock am Skript-Kopf wird pro Plugin angepasst:

PLUGIN_ID="openmontage"
PLUGIN_NAME="OpenMontage"
PLUGIN_VERSION="1.0.0"
PLUGIN_SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"  # nicht ändern
CORE_SRC="$HOME/openmontage-project"   # Quelle für core/
OUTPUT_DIR="$HOME/Desktop"
SYMLINK_CORE=false

# Welche Verzeichnisse aus CORE_SRC als core/<dir>/ ins Paket kommen:
CORE_DIRS=("tools" "pipeline_defs" "lib" "skills" "schemas" "styles" "remotion-composer")
CORE_FILES=("config.yaml" "setup.py")

Das Skript führt diese Schritte aus:

  1. Manifest kopieren
  2. skills/ kopieren
  3. core/-Verzeichnisse via rsync (exkludiert node_modules/, .venv/, __pycache__/, etc.)
  4. hooks/-Skripte kopieren + chmod +x
  5. python/requirements*.txt kopieren
  6. system_prompt.md kopieren
  7. templates/*.md kopieren
  8. zip -r → .irisplugin

Ausgeschlossene Dateien und Verzeichnisse (immer)

.env  .env.local  *.pyc  *.pyo  *.log
__pycache__/  .venv/  venv/  .git/  node_modules/
projects/  assets/  renders/  tests/  .agents/

9. Installations-Ablauf in Iris

Der vollständige Ablauf den Iris beim Import einer .irisplugin-Datei ausführt:

Schritt 1 — Paket validieren

ZIP entpacken in Temp-Verzeichnis → plugin.json lesen → Pflichtfelder prüfen
(id, name, version, skills) → iris_min_version gegen aktuelle Iris-Version
prüfen (Warnung, kein Block)

Schritt 1b — Installations-Sperre (nur mit host_capabilities)

Rechner der Host-Liste (und „Dieser Iris-Server“) per Sonde prüfen →
kein Rechner erfüllt alle Anforderungen UND kein lokaler Weg (`local_mode`) → Installation abgelehnt, nichts angefasst
sonst: host_mode = "local" (kein passender Rechner) oder "pending" (mindestens einer)

Die Sperre steht vor dem ersten Schritt, der etwas anfasst (Treiber-Gruppe, Verzeichnis, Hook). Ein Plugin ohne host_capabilities prüft nichts und läuft ohne Netzwerkzugriff. Das Ergebnis host_mode steht im Ergebnis der Installation und wird dem Hook als IRIS_HOST_MODE übergeben (Kapitel 21).

Schritt 2 — pre_install Hook (optional)

Läuft aus dem Staging-Verzeichnis (Temp). Kann Systemvoraussetzungen prüfen und bei Exit ≠ 0 die Installation abbrechen.

Schritt 3 — Zielverzeichnis anlegen

PLUGIN_DIR="$HOME/Library/Application Support/Iris/plugins/<id>"
# Bei Update: altes Verzeichnis → <id>-<alte-version>.bak

Schritt 4 — Archiv entpacken

unzip -o <paket>.irisplugin -d "$PLUGIN_DIR"

Schritt 5 — Python-venv einrichten

# Iris wählt automatisch das neueste verfügbare Python (Homebrew bevorzugt)
<bestes-python3> -m venv "$PLUGIN_DIR/.venv"
"$PLUGIN_DIR/.venv/bin/pip" install --upgrade pip -q
"$PLUGIN_DIR/.venv/bin/pip" install -r "$PLUGIN_DIR/python/requirements.txt"
ln -sf "$PLUGIN_DIR/skills/<name>" "$HOME/.hermes/skills/<name>"
ln -sf "$PLUGIN_DIR/skills/<name>" "$HOME/.claude/skills/<name>"

Schritt 7 — System-Prompt registrieren

system_prompt.md wird in SystemPromptStore eingetragen. Anzeigename und Verwendungstext kommen aus system_prompt_name und system_prompt_usage im Manifest; fehlen beide, wird name bzw. "<name> Plugin-Prompt" verwendet. Bei Updates wird der vorhandene Eintrag gefunden (auch bei Altinstalls mit dem früheren stabilen Key "plugin-<id>") und überschrieben. Die UUID wird in InstalledPlugin.systemPromptID gespeichert (für Cleanup bei Deinstallation).

Schritt 8 — Dreamer-Templates registrieren

Jede Datei aus plugin.json.templates wird nach ~/Library/Application Support/Iris/dreamer-templates/plugin-<id>-<basename>.md kopiert. Der TemplateMatcherService erkennt sie automatisch. Beim Update werden die alten Dateien zuerst gelöscht.

Schritt 9 — post_install Hook (optional)

Läuft aus dem installierten Plugin-Verzeichnis. Typisch: npm install, Modell-Downloads, externe Konfiguration.

Schritt 10 — Index aktualisieren

~/Library/Application Support/Iris/plugins-index.json

10. Skill-Verknüpfung mit Hermes und Claude Code

SKILL.md schreiben

Pflichtabschnitte:

# <Skill-Name>

Einzeiliger Zweck des Skills.

## Installierter Pfad

Die konkreten Application-Support-Pfade:
- Core: `~/Library/Application Support/Iris/plugins/<id>/core/`
- Venv: `~/Library/Application Support/Iris/plugins/<id>/.venv/`

## Venv aktivieren / Python aufrufen

```bash
source "$HOME/Library/Application Support/Iris/plugins/<id>/.venv/bin/activate"
# oder direkt:
"$HOME/Library/Application Support/Iris/plugins/<id>/.venv/bin/python3" script.py

[Kernfunktionen]

Konkrete Befehle, Beispiele, Ein- und Ausgaben.

VERBOTE

Was der Agent nicht tun darf.


### Namenskollision mit Hermes' eingebauten Skills vermeiden

**Vor dem Benennen eines neuen Skills IMMER prüfen**, ob der Name bereits mit einem
von Hermes' eingebauten Skills kollidiert:

```bash
find ~/.hermes/skills -iname "<name>"

Trifft der gewählte Skill-Name (das Verzeichnis unter skills/ im Manifest, nicht die Plugin-id) einen bereits vorhandenen Hermes-Skill, meldet Hermes bei jedem Turn Ambiguous skill name '<name>': N skills match — das Modell muss jedes Mal neu disambiguieren und lädt dabei teils die falsche/veraltete Version. Das äußert sich wie scheinbar zufälliges Ignorieren der eigenen SKILL.md (falsche Pfade, falsche Defaults, fehlende Verifikationsschritte) und ist ohne Blick in ~/.hermes/logs/agent.log (Suche nach "collision") schwer zu diagnostizieren.

Bereits bestätigte Kollisionsfälle (Skill-Name musste umbenannt werden):

Ursprünglicher Skill-Name Umbenannt zu Kollidierte mit
email email-cleanup Hermes' eingebautem email/himalaya
obsidian obsidian-rag Hermes' eingebautem note-taking/obsidian
heartmula iris-heartmula einem zuvor selbst installierten media/heartmula-Skill

Die Plugin-id selbst ist von dieser Kollision nicht betroffen — sie kann beim naheliegenden Produktnamen bleiben (id: "email", Skill-Name email-cleanup).

Bei einer nachträglichen Umbenennung müssen mehrere Kopien synchron aktualisiert werden, nicht nur das Repo: ~/.hermes/skills/<name>/ (Laufzeit-Symlink), ~/Library/ Application Support/Iris/plugins/<id>/skills/<name>/ und plugins-index.json (Iris' eigene Installationsdatenbank) — sonst überschreibt Iris beim nächsten Sync die Laufzeit-Kopie wieder mit dem alten Namen.


11. System-Prompt und Templates

system_prompt.md

Fertig formulierter Iris-Agenten-Prompt. Wichtige Punkte:

  • Alle Pfade als $HOME/Library/Application Support/Iris/plugins/<id>/... — nie als ~/projekt-verzeichnis/
  • Den Workspace-Pattern erklären
  • Budget-Regeln und API-Call-Regeln festlegen
  • Liste der lokal verfügbaren Tools ohne API-Key

Wann ein Plugin einen Systemprompt braucht

Ein Plugin, das per Chat oder Canvas bedient wird, braucht einen eigenen Systemprompt (system_prompt: true, system_prompt.md, system_prompt_usage in der plugin.json). Die App wählt vor jedem Job per LLM-Klassifikation die passenden Systemprompts, und zwar allein nach deren usage-Satz. Hat das Plugin keinen, tritt es dabei gar nicht an: Nennt die Anfrage „im Canvas“, gewinnt „Canvas System“, das Modell lädt den Skill nie und improvisiert (ein eigenes Skript mit demselben Namen, execute_code, ein Canvas mit Platzhaltern). Die Liste der Skills im Prompt genügt dafür nicht. Nur reine Hintergrund-Plugins kommen ohne aus.

  • usage: ein bis zwei Sätze. Mit dem Anliegen des Nutzers in seinen Worten beginnen, mit „Nicht für …“ gegen Nachbarn abgrenzen (die Klassifikation wählt einen Prompt nur bei direkter Zugehörigkeit).
  • Prompt: den Skill beim Namen nennen und ihn per skill_view laden lassen; die Aufrufe wörtlich in einen Codeblock schreiben (Modelle übernehmen sie); die Anfrage („Zeig mir …“) und die Canvas-Rückmeldung („Canvas-Auswahl bestätigt …“) als getrennte Fälle behandeln; Erfolg nur melden, wenn die Skript-Ausgabe ihn zeigt; kein eigenes Skript, kein execute_code (auf unbeaufsichtigten Plattformen blockiert).
  • Skripte tolerant bauen: Modelle reichen Argumente verkürzt durch (nur den Knopfnamen statt der ganzen Nachricht); das Skript sollte beides verstehen.
  • Prüfen: Der Request-Dump (~/.hermes/sessions/request_dump_*.json) zeigt die tatsächlichen Werkzeugaufrufe; der Chat-Text beweist keine Ausführung.

iris-plugin-dev fragt das im grill-me (Thema systemprompt), erzeugt den Baustein beim Gerüst und prüft, dass Plan und Manifest übereinstimmen.

Templates

Jede .md-Datei in templates/ wird als Dreamer-Template registriert. Der TemplateMatcherService liest den Abschnitt ## Wann verwenden für die LLM-Klassifikation.

Template-Konventionen:

  • Erster # Heading = Titel in der Template-Auswahl
  • ## Wann verwenden = kurze Beschreibung + Bullet-Beispiele für Matcher
  • Kurz und präzise — Template soll Formulierung abnehmen, nicht Denken

12. Optionale Komponenten

Schwere ML-Abhängigkeiten (torch, CUDA, Sprachmodelle) als optionale Komponenten deklarieren, damit der Core-Install schnell bleibt.

In plugin.json deklarieren

"python": {
  "optional": {
    "ai_music": "python/requirements-ai_music.txt"
  }
}

Der Schlüssel (ai_music) erscheint als Toggle in der Plugin-Karte. Der Wert ist der Pfad zur Requirements-Datei relativ zum Plugin-Verzeichnis.

Requirements-Datei schreiben

# requirements-ai_music.txt
# --index-url NICHT verwenden — ersetzt PyPI komplett
# --extra-index-url verwenden — ergänzt PyPI
--extra-index-url https://download.pytorch.org/whl/cpu
torch>=2.0
torchaudio>=2.0
transformers>=4.40.0

Wichtig: --index-url ersetzt den Standard-PyPI-Index vollständig. Pakete die nicht im alternativen Index liegen (z.B. transformers im PyTorch-Index) werden dann nicht gefunden. Immer --extra-index-url verwenden.

Beim Aktivieren in Iris

Iris führt aus:

"$PLUGIN_DIR/.venv/bin/pip" install -r "$PLUGIN_DIR/<pfad-aus-manifest>"

Beim Deaktivieren: venv wird neu angelegt und nur Core-Requirements reinstalliert (kein pip uninstall — zu fehleranfällig bei transitiven Deps).


13. Versionierung und Updates

Versions-Schema

SemVer: MAJOR.MINOR.PATCH

Level Wann
PATCH Bugfixes, Skill-Text verbessert, Requirements aktualisiert
MINOR Neue optionale Komponenten, neue Templates, neue Hooks
MAJOR Inkompatible Core-Änderungen, neue Pflicht-Systemvoraussetzungen

Update-Ablauf

  1. Nutzer importiert neue .irisplugin-Version
  2. Iris vergleicht Versions-Strings
  3. Bei gleicher oder älterer Version: Warnung + Bestätigung nötig
  4. Altes Verzeichnis → <id>-<alte-version>.bak
  5. Neue Version durchläuft die vollständige Install-Pipeline inkl. Hooks
  6. Venv: pip install -r requirements.txt erneut, kein Neu-Anlegen (spart Zeit)

14. Konventionen und Einschränkungen

IDs und Namen

  • id nur Kleinbuchstaben, Ziffern und Bindestriche
  • id ändert sich nie über Versionen hinweg

Hooks

  • Skripte müssen #!/usr/bin/env bash als Shebang haben
  • set -euo pipefail empfohlen
  • Keine interaktiven Prompts (read, dialog) — diese blockieren die Iris-UI → Interaktive Rückfragen sind für V3 geplant (strukturiertes JSON-Protokoll)
  • Langer Output wird nach ~300 Zeichen im Log abgeschnitten
  • sed -i '' nicht verwenden — Iris setzt /opt/homebrew/bin als erstes PATH-Element; ist dort GNU sed installiert (Homebrew gnu-sed), schlägt sed -i '' fehl (GNU-sed-Syntax: kein leeres Extension-Argument). Portabler Ersatz für In-Place-Editing:
    tmpfile=$(mktemp)
    grep -v 'MUSTER' "$DATEI" > "$tmpfile" || true
    mv "$tmpfile" "$DATEI"
    

Secrets

  • Niemals .env oder API-Keys ins Paket einbetten
  • API-Keys werden vom Nutzer in der Iris-Keychain-Einstellung hinterlegt
  • Hooks können $HOME/.iris/env.sh sourcen wenn nötig (User muss diese Datei selbst anlegen)

Paketgröße

  • Ziel: < 10 MB (ohne ML-Gewichte, ohne node_modules)
  • node_modules kommt nie ins Paket — post_install-Hook führt npm install aus
  • ML-Modell-Gewichte (ONNX, PyTorch) nie ins Paket — werden zur Laufzeit aus HuggingFace-Cache oder GitHub Releases geladen

15. Settings-Panel & Direktzugriff (Web-Views)

Stand 2026-09-12. Das ursprüngliche native Settings-Panel-System (kompiliertes macOS-.framework, native_framework_path/IrisSettingsPanelProvider) ist veraltet und wird nicht mehr für neue Plugins verwendet — ein Konfigurations-Panel läuft heute über eine einfache HTML/JS-Datei, die sowohl auf dem Mac als auch auf dem Linux-Server läuft (kein natives Framework mehr nötig). Kein aktuell gepflegtes Plugin nutzt noch native_framework_path ausschließlich für ein Settings-Panel.

Konzept: Warum Konfiguration und Anwendung trennen?

Vor diesem Feature hatte jedes Plugin genau EIN Panel — egal ob es sich um eine einmalige Einrichtung handelte (SSH-Ziel eintragen, API-Key hinterlegen) oder um etwas, das ein Nutzer täglich mehrfach braucht (eine Stimme klonen, ein neues Passwort hinterlegen). Beides lag hinter demselben Weg: Einstellungen → Plugins → Plugin-Karte suchen → "Konfigurieren" klicken. Für ein Setup, das man einmal macht und nie wieder anfasst, ist das ein akzeptabler Umweg. Für etwas, das zum Alltagswerkzeug wird, ist es unnötige Reibung — der Nutzer muss sich merken, in welchem der potenziell vielen installierten Plugins die gesuchte Funktion steckt, und sich jedes Mal durch dieselbe Navigationskette klicken.

Die Trennung in zwei Rollen löst genau das:

  • Konfigurieren (settings_web_view_path) bleibt dort, wo Einrichtung hingehört — im Plugin-Modul (Einstellungen → Plugins). Dieser View wird immer dann aufgerufen, wenn das Plugin selbst parametrisiert wird — also wenn der Nutzer bewusst in die Plugin-Verwaltung geht, um etwas an der GRUNDEINSTELLUNG des Plugins zu ändern (Zugangsdaten, Ziel-Host, Modell-Wahl, Timeouts). Das passiert selten, meist einmal bei der Installation und dann nur noch bei Bedarf.
  • Anwenden (usage_web_view_path) taucht als eigener Menüpunkt im neuen Plugins-Chevron in der Toolbar auf (Blitz-Symbol, direkt neben dem Einstellungen-Zahnrad) — ein Klick, ohne durch die Plugin-Liste suchen zu müssen. Hierhin gehört alles, was ein Nutzer im Alltag tatsächlich BENUTZT, nicht nur einmalig einrichtet.

Die Leitfrage beim Entwerfen eines neuen Plugins: "Mache ich das einmal, oder mache ich das ständig?" Alles aus der ersten Kategorie gehört in settings_web_view_path, alles aus der zweiten in usage_web_view_path. Ein Plugin muss diese Frage nicht zwingend mit "beides getrennt" beantworten — siehe den Fallback-Fall weiter unten, wenn Einrichten und Benutzen ohnehin dieselbe Oberfläche sind.

Zwei getrennte Rollen: Konfigurieren vs. Anwenden

Ein Plugin kann bis zu zwei unabhängige Web-Views deklarieren, die an zwei unterschiedlichen Stellen in der Iris-GUI auftauchen:

Feld Rolle Erreichbar über
settings_web_view_path Konfigurieren — einmalige Einrichtung (Zugangsdaten, SSH-Ziel, API-URL, …) Einstellungen → Plugins → "Konfigurieren"-Button auf der Plugin-Karte
usage_web_view_path Anwenden — die Seite, die man im Alltag tatsächlich benutzt Der neue "Plugins"-Chevron (Blitz-Symbol) in der Toolbar, direkt neben dem Einstellungen-Zahnrad

Beide Felder sind unabhängig optional:

  • Nur settings_web_view_path gesetzt → das Plugin hat ein Konfigurations-Panel, aber keinen Direktzugriff-Menüpunkt (Standardfall für reine Einrichtungs-Plugins).
  • Beide gesetzt, plus quick_access_label → zwei komplett getrennte Seiten für zwei komplett getrennte Zwecke (z. B. ein Plugin, dessen Konfiguration ein SSH-Ziel abfragt, dessen tägliche Nutzung aber eine ganz andere Oberfläche braucht).
  • Nur settings_web_view_path + quick_access_label, kein usage_web_view_path → der Chevron-Eintrag öffnet automatisch dieselbe Seite wie "Konfigurieren" (Fallback). Sinnvoll, wenn Einrichten und Benutzen dieselbe Oberfläche sind — Beispiel: iris-web-automation verwaltet Zugangsdaten; das Anlegen eines neuen Kontos IST bereits die alltägliche Nutzung, eine zweite Seite dafür wäre unnötige Redundanz.

Wie man die beiden Views anlegt

Beide Views sind reine HTML/JS-Dateien — kein natives Framework, keine Swift-Message-Bridge, kein Build-Schritt außer "Datei existiert im Paket".

1. Dateien anlegen (Konvention: beide unter web/, eigene Dateinamen):

plugins/<mein-plugin>/
  plugin.json
  web/
    settings.html        ← Konfigurieren
    apply.html            ← Anwenden (Name frei wählbar — muss nur zu
                             usage_web_view_path passen)

2. In plugin.json referenzieren:

{
  "has_settings_panel": true,
  "settings_web_view_path": "web/settings.html",
  "usage_web_view_path": "web/apply.html",
  "quick_access_label": "Meine tägliche Aufgabe",
  "quick_access_icon": "bolt.fill"
}

has_settings_panel: true ist Pflicht, sobald settings_web_view_path gesetzt ist (das Build-Skript bricht sonst mit einer klaren Fehlermeldung ab, falls es das generische deployment/build-irisplugin.sh-Template verwendet — siehe unten). usage_web_view_path selbst braucht kein zusätzliches Bool-Feld, es wird allein an seiner Anwesenheit erkannt.

3. Ins Paket aufnehmen (Build-Skript):

Plugins mit einem eigenen, handgeschriebenen build-irisplugin.sh (wie iris-web-automation) brauchen dafür meist gar keine Änderung — wenn das Skript bereits den kompletten web/-Ordner rekursiv kopiert (cp -R "$PLUGIN_SRC/web/." "$BUILD_DIR/web/"), landet eine neu hinzugefügte apply.html automatisch mit im Paket, sobald plugin.json darauf verweist.

Plugins, die stattdessen das geteilte Template (~/Documents/2ndbrain/deployment/build-irisplugin.sh, z. B. ComfyUI Bridge, OpenMontage, Mask Editor Reference) verwenden: dieses Template generiert plugin.json bisher aus Variablen wie SETTINGS_WEB_VIEW_PATH und kennt usage_web_view_path/quick_access_label/quick_access_icon/driver_group noch nicht als eigene Variablen (Stand 2026-09-12). Für ein Plugin auf Template-Basis entweder das Template um die entsprechenden Variablen erweitern (gleiches Muster wie SETTINGS_WEB_VIEW_PATH, siehe dessen Code dort) oder — schneller für ein einzelnes Plugin — das generierte plugin.json nach dem Build-Lauf einmalig um die fehlenden Felder ergänzen, wie es aktuell bei iris-web-automation/qwen-tts-mac/qwen-tts-linux gehandhabt wird (dort komplett handgeschriebenes plugin.json, keine Template-Generierung).

4. Inhalt der HTML-Datei — der Vertrag:

Beide View-Typen folgen demselben, sehr einfachen Kontrakt — kein window.irisPluginInit, kein postMessage, keine native Bridge (das ist nur für Canvas-Block-web_view_path nötig, siehe Abschnitt 5-adjacent — nicht für Settings/Usage):

<script>
  // Iris hängt bei jedem Öffnen einen frischen, kurzlebigen Token an die URL an
  const cfgToken = new URLSearchParams(window.location.search).get("cfg_token");
  const apiBase = window.location.origin + "/plugins/mein-plugin";

  async function loadConfig() {
    const res = await fetch(`${apiBase}/config?token=${cfgToken}`);
    return (await res.json()).config;
  }

  async function saveConfig(config) {
    await fetch(`${apiBase}/config?token=${cfgToken}`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ config }),
    });
  }
</script>

Der Token authentifiziert ausschließlich gegen /plugins/<id>/*-Routen des eigenen Plugins (siehe "Auth-Mechanismus" unten) — die Seite bekommt nie den echten, dauerhaften Bearer-Token zu sehen.

Panel-Backend ohne Iris-Änderung: panel_api

Dateien im Workspace des Chats: Schreibt ein panel_api-Befehl Dateien in den Workspace des Chats, die mit einem geteilten Chat (grüner Daumen) auf den anderen Rechner sollen, meldet er das in der Antwort mit "workspace_changed": true; der Server stößt dann den Export an (Vorbild: iris-browser, Tab-Liste in .iris-browser-tabs.json, iris/Iris#328).

Das ist der Standardweg. Ein neues Plugin bekommt keine eigenen Swift-Routen und keine eigenen Python-Routen auf dem Linux-Server für sein Panel — Änderungen am Iris-Hauptcode sind für ein Plugin nur in absoluten Ausnahmefällen vorgesehen. (Die bestehenden Sonderrouten von iris-web-automation, heartmula und whisper für Test und Installation sind Altlasten und werden bei Gelegenheit auf diesen Mechanismus umgestellt; deren Zustand für den Marktplatz läuft schon über panel_api (config-status).)

Das Plugin deklariert im Manifest ein Skript und die Befehlsnamen, die es bedient:

{
  "has_settings_panel": true,
  "settings_web_view_path": "web/settings.html",
  "panel_api": {
    "script": "skills/mein-skill/panel_api.py",
    "commands": ["status", "save", "test-connection"],
    "timeout": 60
  }
}

Die Seite ruft POST /plugins/<id>/api/<befehl>?token=<cfg_token> mit einem JSON-Body auf. Iris startet <python> <script> <befehl>:

Punkt Vertrag
Eingabe Der Request-Body kommt ausschließlich über Stdin (nie als Argument, nie im Log — er kann ein Passwort enthalten). Maximal 64 KB.
Ausgabe Genau ein JSON-Objekt auf Stdout, Exit-Code 0. Iris gibt es unverändert als Antwort zurück.
Fehler Exit-Code ≠ 0 oder Ausgabe ohne JSON-Objekt → HTTP 500 mit {"error": "<letzte Zeile von stderr>"}. Die Seite zeigt json.error. Ein fachliches Ergebnis (z. B. "Verbindung fehlgeschlagen") gibst du besser als Exit 0 mit {"ok": false, ...} zurück, damit die Seite Details anzeigen kann.
Timeout timeout Sekunden (Standard 30, begrenzt auf 1–300), danach wird das Skript beendet (HTTP 500).
Whitelist Nur Befehle aus commands laufen (sonst 404). Das Skript muss im Plugin-Verzeichnis liegen (kein .., keine Symlink-Flucht).
Python Das venv des Plugins (.venv/bin/python3), sonst die Homebrew-bevorzugende Auswahl des Installers. Ohne venv also nur Standardbibliothek verwenden.
Vertrauen Der cfg_token ist an die Plugin-ID gebunden: Eine Seite kann nur Befehle ihres eigenen Plugins auslösen.

Referenz-Umsetzung: iris-nextcloud-cookbook (skills/iris-nextcloud-cookbook/panel_api.py, web/settings.html, Tests in tests/test_panel_api.py). Die Iris-Seite ist handlePluginPanelAPI in IrisRemoteServer.swift (Mac), das Gegenstück auf dem Linux-Server ist plugin_panel_api in linux-server/iris-server/app/plugins_routes.py (gleicher Vertrag; das Skript bekommt zusätzlich die Umgebungsvariable IRIS_APP_SUPPORT mit dem App-Support-Ordner). Ein panel_api-Panel läuft damit auf beiden Servern.

config-status ist ein reservierter Befehlsname: Jedes Plugin mit Konfigurationsfenster führt ihn in commands und meldet damit seinen Zustand (Grün, Gelb, Rot) an den Marktplatz, ohne Eingabe, ohne SSH, schnell (Zeitlimit 10 Sekunden). Vertrag und Beispiele: Kapitel 21.

Ein Plugin, das panel_api nutzt, braucht eine Iris-Version, die diese Route enthält; ein Versionsmarker dafür existiert noch nicht (iris_min_version warnt nur und ist derzeit nicht an eine echte Iris-Version gebunden).

Beispiel: iris-web-automation

Das real installierte iris-web-automation-Plugin (Login+Navigation+Aktion- Automatisierung für Anbieter-Webseiten) nutzt heute bewusst den Fallback-Fall: nur settings_web_view_path ist gesetzt, usage_web_view_path fehlt. Der Grund liegt direkt am Inhalt der einzigen vorhandenen Seite (web/settings.html): sie verwaltet Anbieter-Zugangsdaten — Konto anlegen (URL/Benutzername/Passwort), CSV-Bulk-Import aus dem Passwort-Manager, Konto löschen, Passwort abrufen. Genau diese Aufgabe ist gleichzeitig die Einrichtung UND die alltägliche Nutzung — ein neues Konto anlegen, wenn ein Passwort sich ändert, IST bereits "Anwenden". Eine zweite, inhaltlich fast identische Seite nur für den Chevron zu bauen, wäre reine Duplizierung ohne echten Nutzen.

{
  "id": "iris-web-automation",
  "has_settings_panel": true,
  "settings_web_view_path": "web/settings.html",
  "quick_access_label": "Logindaten und Web-Automatisierung",
  "quick_access_icon": "key.fill"
}

Der Plugins-Chevron zeigt trotzdem einen Eintrag ("Logindaten und Web-Automatisierung") — weil quick_access_label gesetzt ist —, öffnet dabei aber laut Fallback-Regel automatisch dieselbe settings.html, statt eine zweite Seite zu verlangen.

Wann würde sich eine echte Trennung für dieses Plugin lohnen? Sobald das Plugin um etwas erweitert wird, das man wirklich nur EINMAL einstellt und dann selten wieder anfasst — zum Beispiel globale Automatisierungs-Einstellungen (Standard-Timeout, ob bei einem Fehler ein Screenshot gespeichert wird, Headless- vs. sichtbarer Browser-Modus). In diesem Fall wäre die sinnvolle Aufteilung:

View Inhalt Warum dort
settings_web_view_path (Konfigurieren) Globale Automatisierungs-Einstellungen: Timeout, Screenshot-bei-Fehler, Browser-Modus Einmal eingestellt, danach fast nie wieder angefasst
usage_web_view_path (Anwenden) Die bestehende Zugangsdaten-Verwaltung (Konten anlegen/löschen, CSV-Import, Passwort abrufen) Das ist der Teil, den man tatsächlich regelmäßig braucht — verdient den Ein-Klick-Zugriff über den Chevron

Genau dieser Fall — "die bereits vorhandene Seite ist eigentlich schon 'Anwenden', eine neue, seltener gebrauchte Seite kommt als 'Konfigurieren' dazu" — ist der typische Weg, ein bestehendes Ein-Seiten-Plugin nachträglich sauber aufzuteilen, statt von Anfang an zu raten, was gebraucht wird.

quick_access_label/quick_access_icon — der Plugins-Chevron

Ein Plugin erscheint im Plugins-Chevron nur, wenn quick_access_label gesetzt ist:

{
  "quick_access_label": "Logindaten und Web-Automatisierung",
  "quick_access_icon": "key.fill"
}

quick_access_icon ist ein SF-Symbol-Name (z. B. "waveform", "key.fill", "gearshape") und optional — ohne Angabe wird ein generisches Puzzle-Icon verwendet. quick_access_label allein genügt, um einen Eintrag zu bekommen; welche Seite sich dabei öffnet, entscheidet die Fallback-Logik oben.

Native Ausnahme: Ein Plugin, dessen tägliche Nutzung eine eigene, native SwiftUI- Oberfläche ist statt einer Web-View (aktuell einziges Beispiel: die qwen-tts- Treibergruppe, deren Stimmen-Verwaltung ein natives Panel ist, keine HTML-Seite), kann trotzdem quick_access_label setzen — der Client erkennt diesen Fall clientseitig über driver_group und öffnet das native Panel statt einer Web-View. Für ein neues Plugin ohne native Oberfläche ist das nicht relevant; einfach usage_web_view_path (oder nur settings_web_view_path mit Fallback) verwenden.

driver_group — austauschbare Treiber-Plugins

Manche Fähigkeiten haben mehrere alternative Implementierungen mit identischer Kernschnittstelle, aber unterschiedlichem Backend (lokal vs. Remote-GPU-Host, z. B. qwen-tts-mac vs. qwen-tts-linux). Beide tragen dasselbe driver_group:

"driver_group": "qwen-tts"

Installiert der Nutzer ein Plugin mit derselben driver_group wie ein bereits installiertes (aber anderer id), wird das alte automatisch deinstalliert — wie ein Hardware-Treiberwechsel. Ein Plugin ohne Alternativen braucht dieses Feld nicht.

Auth-Mechanismus (kurz — für Plugin-Autoren meist irrelevant)

Beide Web-View-Typen laufen über denselben kurzlebigen cfg_token-Mechanismus: Iris mintet bei jedem Öffnen einen frischen, plugin-gebundenen Token und hängt ihn als Query-Parameter an die Proxy-URL. Die Seite selbst liest cfg_token aus window.location.search und hängt ihn an ihre eigenen fetch()-Aufrufe gegen die Plugin-eigenen Config-Aktions-Routen (GET/POST /plugins/:id/config, plus beliebige weitere /plugins/:id/<action>-Routen) an — die echte, dauerhafte Bearer-Auth bekommt die Seite nie zu sehen. Ein für "Konfigurieren" gemintete Token authentifiziert gleichermaßen für "Anwenden" (und umgekehrt) — beide Web-Views desselben Plugins sind gleich vertrauenswürdig.


Farben der Seiten: Theme-Variablen --iris-*

Web-Seiten von Plugins nehmen die Farben aus den CSS-Variablen der App (--iris-bg, --iris-text, --iris-accent …, mit Rückfallwert), damit sie Hell/Dunkel und das gewählte Theme (Iris, Graphit, Papier) mitmachen. Liste, Schreibweise und das Skript zum Umstellen bestehender Seiten: _shared/iris-web-theme/README.md.

16. Referenz-Plugin: Web-Automation

plugins/iris-web-automation/ ist das aktuelle Referenz-Plugin — es nutzt deutlich mehr vom Plugin-System als das ältere OpenMontage-Beispiel (kein natives .framework, kein Optionale-Komponenten-Mechanismus, keine Dreamer-Templates dort), dafür aber Skills, ein plugin-weites venv, einen Lifecycle-Hook, ein Web-View-Settings-Panel UND den Plugins-Chevron-Direktzugriff (Abschnitt 15) — die Kombination, die ein neues Plugin heute typischerweise braucht.

plugins/iris-web-automation/
├── build-irisplugin.sh
├── plugin.json
├── docs/
│   └── anbieter-config-beispiel.json
├── hooks/
│   └── post_install.sh          playwright install chromium (nur Mac)
├── skills/iris-web-automation/
│   ├── SKILL.md
│   ├── runner.py                 Playwright-Automatisierungs-Engine
│   ├── credentials.py            Zugangsdaten-CRUD (secrets_store)
│   ├── web_automation_cli.py      CLI-Einstieg für Hermes
│   └── requirements.txt
├── system_prompt.md
└── web/
    └── settings.html              Konfigurieren-UND-Anwenden-View (Fallback, siehe unten)

plugin.json (gekürzt):

{
  "id": "iris-web-automation",
  "name": "Web-Automation",
  "version": "0.1.4",
  "author": "alf",
  "iris_min_version": "2.0.0",
  "has_settings_panel": true,
  "settings_web_view_path": "web/settings.html",
  "quick_access_label": "Logindaten und Web-Automatisierung",
  "quick_access_icon": "key.fill",

  "skills": ["iris-web-automation"],
  "system_prompt": true,
  "system_prompt_name": "Web-Automation",

  "python": {
    "venv": true,
    "requirements": "skills/iris-web-automation/requirements.txt"
  },

  "hooks": {
    "post_install": "hooks/post_install.sh"
  }
}

Paket bauen:

bash ~/Documents/2ndbrain/plugins/iris-web-automation/build-irisplugin.sh
# → ~/Desktop/iris-web-automation-0.1.4.irisplugin  (~26 KB)

Anders als OpenMontage (das vom generischen deployment/build-irisplugin.sh-Template gebaut wird) hat dieses Plugin ein eigenes, handgeschriebenes Build-Skript — es kopiert plugin.json unverändert (keine Variablen-Generierung), zieht requirements.txt direkt aus skills/iris-web-automation/ (Python-Requirements liegen hier NEBEN dem Skill, nicht in einem separaten python/-Ordner) und kopiert web/ rekursiv mit — der praktische Grund, warum ein neuer usage_web_view_path für dieses Plugin ohne jede Build-Skript-Änderung funktionieren würde (siehe Abschnitt 15, "Wie man die Views anlegt").

Was der post_install-Hook tut: installiert die Playwright-Chromium-Binaries ins plugin-eigene venv (playwright install chromium) — dauert beim ersten Install 1-2 Minuten. Läuft bewusst nur auf dem Mac; auf dem Linux-Server übernimmt stattdessen ein dedizierter Docker-Container (linux-server/iris-server/web-automation/) das komplette Chromium-Setup selbst, der Hook wird dort laut plugin_installer.pys Moduldoc ohnehin nie ausgeführt.

Installiert unter:

~/Library/Application Support/Iris/plugins/iris-web-automation/

Vollständig installierte Komponenten:

  • Python-venv mit Playwright + Chromium-Browser-Binaries
  • Skill-Symlinks für Hermes und Claude Code (iris-web-automation)
  • System-Prompt in SystemPromptStore ("Web-Automation")
  • web/settings.html als Konfigurieren-Panel (Einstellungen → Plugins) UND als Plugins-Chevron-Eintrag ("Logindaten und Web-Automatisierung") — siehe Abschnitt 15 für die Begründung, warum dieses Plugin bewusst den Fallback statt einer echten usage_web_view_path-Trennung nutzt.
  • Keine Dreamer-Templates, keine optionalen Komponenten — beide Mechanismen sind für dieses Plugin schlicht nicht relevant (kein Vorlagen-Anwendungsfall, keine schweren optionalen ML-Abhängigkeiten).

17. Scheduled-Job-Vorlagen (System-Vorlagen)

Stand 2026-09-13. Ersetzt eine frühere, nie wirklich benutzte Variante (scheduled_job, Singular-Objekt), die bei der Installation sofort einen deaktivierten Hintergrund-Job anlegte. In der Praxis (erster echter Nutzungsversuch: finanzen-automation) erwies sich das als unangenehm: eine Titel-Kollision mit einem bereits vorhandenen Job ließ die Anlage still ausfallen, ohne jeden Hinweis — der Nutzer musste den Job danach komplett von Hand anlegen und dabei den exakten Shell-Befehl selbst eintippen. Die Vorlagen-Karte behebt beides: nichts fällt je still aus (eine Vorlage verschwindet nur, sobald ihr eigener Job schon existiert — das ist beabsichtigt, kein Fehler), und der Nutzer tippt nie einen Befehl selbst ab.

Konzept

Ein Plugin kann beliebig viele Hintergrund-Job-Vorlagen deklarieren. Jede erscheint als eigene Karte im "Hintergrund-Jobs"-Panel unter "System-Vorlagen" — derselbe Abschnitt, in dem auch die drei eingebauten Vorlagen ("Vault-Wartung", "Vault Attachment Repair", "Index Job-Aufgaben") stehen. Ein Klick auf die Karte öffnet den normalen Bearbeiten-Dialog, vorausgefüllt mit den Werten aus der Vorlage — der Nutzer sieht/kann Titel, Zeitplan und Kommando vor dem Speichern noch anpassen, muss aber nichts selbst eintippen. Es entsteht kein Job, bevor der Nutzer aktiv auf "Speichern" klickt — ein Plugin installieren aktiviert nie von selbst eine wiederkehrende Automatisierung.

Eine Vorlagen-Karte verschwindet automatisch, sobald bereits ein Job mit demselben title existiert (Abgleich nach Titel, wie bei den drei eingebauten Vorlagen) — kein Duplikate-Risiko, kein manuelles Aufräumen nötig.

Manifest-Feld

"scheduled_job_templates": [
  {
    "title": "Rechnungslauf",
    "description": "Verarbeitet neue Rechnungen aus \"03 Bereiche/Finanzen/Anbieter/\" — ein Job pro Anbieter, nacheinander. Täglich um 09:00.",
    "icon": "eurosign.circle",
    "kind": "shellScript",
    "shell_command": "python3 ~/.hermes/skills/finanzen-automation/finanzen_orchestrator.py",
    "run_in_hermes": true,
    "default_schedule": {
      "kind": "daily",
      "interval_value": 1,
      "hour": 9,
      "minute": 0
    }
  }
]
Feld Typ Pflicht Beschreibung
title string ✅ Job-Name — erscheint als Karten-Titel UND als Titel des später angelegten Jobs. Muss innerhalb der Iris-Installation eindeutig sein (dient als Abgleichs-Schlüssel).
description string — Karten-Untertitel. Ohne Angabe zeigt die Karte stattdessen "Von Plugin „"".
icon string — SF-Symbol-Name für die Karte (z.B. "eurosign.circle", "waveform"). Ohne Angabe ein generisches Puzzle-Icon.
kind string — "shellScript" für ein deterministisches Skript-Kommando, oder weggelassen/"customPrompt" für einen LLM-gesteuerten Hermes-Prompt. Siehe unten.
prompt_text string — Nur bei kind = "customPrompt" (oder weggelassen). Der Prompt-Text, der bei jedem Lauf an Hermes geschickt wird.
shell_command string — Nur bei kind: "shellScript". Läuft identisch auf Mac und Linux, solange der Befehl ~/.hermes/skills/<name>/...-Pfade nutzt (siehe unten) — keine Plattform-Fallunterscheidung im Kommando selbst nötig.
run_in_hermes bool — Nur bei kind: "shellScript", nur auf Linux relevant (No-op auf dem Mac) — läuft das Skript im iris-hermes-scheduled-Container statt im schlanken server-Container, der kein ~/.hermes hat. Für jedes Skript unter ~/.hermes/skills/... fast immer true.
default_schedule.kind string — "daily", "weekly", "everyHours" oder "everyMinutes". Ohne Angabe: "everyHours" alle 6 Stunden.
default_schedule.interval_value int — Bei everyHours/everyMinutes das Intervall; bei daily/weekly ungenutzt.
default_schedule.hour int — Nur bei daily/weekly. Ohne Angabe: 3 (03:00).
default_schedule.minute int — Nur bei daily/weekly. Ohne Angabe: 0.

Warum kind: "shellScript" für deterministische Automatisierungen

Ein customPrompt-Job lässt ein LLM jeden Lauf neu interpretieren — für eine Aufgabe, die exakt und ohne Ermessensspielraum ablaufen muss (z.B. "verarbeite jede noch offene Rechnung genau einmal"), ist das ein echtes Risiko: das Modell kann einen Lauf überspringen, doppelt verarbeiten oder eine Rückfrage improvisieren, wo keine hingehört (siehe AUTOMATISATION.md, "Determinismus vor LLM-Improvisation" — dasselbe Prinzip, das auch für SKILL.md-Anleitungen gilt). kind: "shellScript" startet stattdessen ein reines Skript — kein LLM-Turn im Scheduler-Pfad selbst, die eigentliche Fachlogik (inkl. etwaiger eigener Hermes-Jobs, die das Skript selbst startet) bleibt beim Plugin.

Plattform-Portabilität des Shell-Kommandos

Ein Skript-Pfad wie ~/.hermes/skills/<skill-name>/<script>.py ist auf beiden Plattformen identisch gültig — Iris richtet nach der Installation auf dem Mac einen echten Symlink und auf dem Linux-Server eine echte Kopie an derselben logischen Stelle ein (siehe Abschnitt 6). Ein Plugin-Autor muss deshalb nie zwei verschiedene shell_command-Werte für Mac/Linux pflegen oder zur Laufzeit die Plattform erkennen — derselbe String funktioniert auf beiden.

Klick-Verhalten (für Plugin-Autoren nicht weiter relevant)

Ein Tap auf die Karte baut aus der Vorlage einen unfertigen ScheduledJob (Titel, Payload, Zeitplan — immer isEnabled: false) und öffnet damit denselben Bearbeiten-Dialog, den auch "Bearbeiten" auf einem bestehenden Job öffnet. Erst der explizite "Speichern"-Klick legt den Job wirklich an (lokal via ScheduledJobManager.addJob, remote via POST /scheduled-jobs) — für ein Plugin ändert sich dadurch nichts an der Persistenz selbst, nur der Weg dorthin.


18. Veröffentlichung im offiziellen Repository (Plugin-Registry)

Stand 2026-10-09: ersetzt durch Kapitel 19. Statt Git-Tags und Releases im Quellrepo liegen die fertigen Pakete in einem eigenen Registry-Repo (iris/iris-plugin-registry); publish-plugins.sh (statt des hier geplanten publish-plugin.sh) ist gebaut. Der Rest dieses Kapitels ist der frühere Entwurf.

Stand 2026-09-16 — GEPLANT, noch nicht gebaut. Vollständige Architektur- und Entscheidungsklärung per grill-me abgeschlossen, dokumentiert in der Iris-App-Plandatei frontend/macos-iris/obsidian-docs/plans/PLUGIN-REGISTRY.md (Iris-Repo, nicht hier). Bis dahin bleibt "aus Datei" (.irisplugin-Import über NSOpenPanel) der einzige Installationsweg in der Iris-GUI — dieser Abschnitt beschreibt den geplanten Zielzustand, keinen bestehenden.

Konzept

Zusätzlich zum "aus Datei"-Import bekommt das Iris-Plugin-Panel einen "Repository"-Tab, der gegen genau dieses Repo (iris-plugins, Forgejo-Organisation iris) geht — Iris fragt die Forgejo-API anonym nach veröffentlichten Versionen ab und installiert/aktualisiert direkt daraus, ohne Umweg über eine manuell heruntergeladene Datei. Dieses Repo bleibt dabei die einzige Quelle der Wahrheit — kein zweites Registry-Repo, kein separates registry.json.

Versionierung: Ein Git-Tag pro Plugin-Version

<plugin-id>@<version>

Beispiel: blender-asset-generator@1.2.0. @ als Trenner (nicht -v) ist bewusst gewählt — mehrere Plugin-IDs in diesem Repo enthalten selbst Bindestriche (blender-asset-generator, qwen-tts-linux, translation-matrix, iris-web-automation, mask-editor-reference, ...), ein -v-Trenner wäre beim Zurückparsen mehrdeutig gewesen.

Jeder Tag bekommt ein Forgejo-Release mit dem gebauten .irisplugin als Release-Asset angehängt — anders als reine Text-Templates (siehe ForgejoLinuxServerRegistry.swift im Iris-Repo, das strukturelle Vorbild für den App-seitigen Client) braucht ein binäres Archiv Forgejos Asset-Upload statt eines einfachen /raw/-Datei-Fetches.

Geplanter Publish-Workflow: publish-plugin.sh

Neues Skript im Repo-Root (analog zu deployment/build-and-push.sh im iris-linux-server-Repo), manuell ausgeführt, sobald ein Plugin release-reif ist — kein automatischer CI-Trigger bei jedem Push, bewusste Entscheidung für Kontrolle darüber, wann ein Zwischenstand als "offiziell installierbar" gilt:

./publish-plugin.sh <plugin-id>

Geplanter Ablauf: liest version aus plugins/<plugin-id>/plugin.json, ruft dessen build-irisplugin.sh auf, erstellt den Git-Tag <plugin-id>@<version>, pusht ihn, legt per Forgejo-API ein Release an und lädt das gebaute .irisplugin als Asset hoch. Braucht ein Forgejo-Write-Token ($FORGEJO_TOKEN, siehe ~/.zshrc) — lebt ausschließlich auf der Entwicklungsmaschine, landet nie in der Iris-App selbst (die App liest nur anonym/read-only).

Bekannte, bewusst vertagte Einschränkung

PluginManager.swifts nicht-interaktiver Installationspfad (interactive: false, genutzt für Batch-/Hintergrund-Installationen) lässt install_config-Antworten aktuell immer leer, auch bei einem bereits installierten Plugin mit gespeicherten Antworten. Der geplante "Alle Plugins updaten"-Knopf im Repository-Tab nimmt Plugins mit install_config-Fragen deshalb bewusst von der Massen-Aktualisierung aus (aktuell betroffen: ios-install, marker) — die müssen vorerst einzeln über den bestehenden "Konfigurieren"-Weg aktualisiert werden, bis diese Plugins später ans neue System angepasst werden. Details siehe PLUGIN-REGISTRY.md.


19. Plugins veröffentlichen: publish-plugins.sh und das Registry-Repo

Stand 2026-10-21. Skript, Index-Helfer und Registry-Repo sind in Betrieb; veröffentlicht sind die freigegebenen Plugins (./publish-plugins.sh --list). Die Leseseite und der Marktplatz laufen im Linux-Server und im Mac-Server und sind in Iris, IrisClient und IrisRemote (iOS) eingebaut. Aufbau, Entscheidungen, Index-Format und offene Fragen: PLUGIN-REGISTRY-REPO.md; was ein Plugin für den Marktplatz mitbringen muss: Kapitel 20.

Die aktuellen, fertig gebauten Pakete liegen in einem eigenen Forgejo-Repo, iris/iris-plugin-registry (öffentlich lesbar): eine index.json und darunter packages/<plugin-id>/<plugin-id>-<version>.irisplugin. Das Quellrepo (dieses hier) bleibt der Ort der Entwicklung. Server und App lesen nur die Registry, anonym.

Katalog für den Marktplatz: category, requires, store/

Der Index trägt je Plugin category und requires (aus der plugin.json im Paket) und oben categories (Name, Symbol, Farbe, Reihenfolge; Quelle: registry-tools/categories.json). Optionales Material zum Stöbern liegt im Verzeichnis store/ des Plugins im Quellbaum, nicht im Paket: publish-plugins.sh legt es nach store/<id>/ in die Registry (Einzelheiten und Gestaltungsregeln: Kapitel 20). Erlaubt sind icon.png|svg|webp (bis 512 KB), description.md, hero.html und changelog.md (je bis 64 KB; die vollständige Changelog, ein Abschnitt je Version) und screenshots/<Datei> (png, jpg, webp, gif, mp4, webm; höchstens 12, je bis 8 MB), zusammen höchstens 25 MB. Anderes lehnt das Veröffentlichen ab. Geänderte Medien und Texte werden auch ohne neue Version veröffentlicht. Dazu kommt die Anleitung: Die Seiten aus user_docs der plugin.json (Ordner docs/ im Quellbaum) rendert das Skript zu store/<id>/anleitung/<name>.html (Option --docs des Index-Helfers; der Index trägt store.docs, nur mit Anleitung). Hat das Manifest ein Feld changelog, steht der Text im history-Eintrag der Version und in changelog des Plugins. Format im Index: PLUGIN-REGISTRY-REPO.md.

publish-plugins.sh

Im Verzeichnis plugins/, von Hand ausgeführt (nie automatisch bei einem Push):

./publish-plugins.sh marker heartmula          # bestimmte Plugins
./publish-plugins.sh --all                     # alle freigegebenen (mit .include); Unveränderte werden übersprungen
./publish-plugins.sh --list                    # zeigen, welche Plugins freigegeben sind (ändert nichts)
./publish-plugins.sh --all --dry-run           # bauen und zeigen, was sich ändern würde; die Registry bleibt unverändert

Ablauf je Plugin: bauen (dessen build-irisplugin.sh, Ausgabe in ein temporäres Verzeichnis) → in index.json eintragen und das Paket ablegen (registry-tools/registry_index.py add) → alte Versionen aufräumen (die letzten 5 bleiben) → am Ende die ganze Registry prüfen (check: jede Datei da, Prüfsumme stimmt), committen und pushen.

Option Bedeutung
--registry-dir PFAD lokaler Klon des Registry-Repos (Standard: $IRIS_PLUGIN_REGISTRY_DIR, sonst ~/Documents/2ndbrain/plugin-registry)
--keep N so viele Versionen je Plugin behalten (Standard 5)
--dry-run nichts im Registry-Repo ändern (arbeitet auf einer Kopie)
--no-push / --no-pull nur committen, nicht pushen / vorher kein git pull

Regeln:

  • Freigabe per .include. Nur Plugins mit einer (leeren) Datei .include im Plugin-Verzeichnis werden veröffentlicht; --all nimmt genau diese, ein ausdrücklich genanntes Plugin ohne .include wird abgelehnt. Das Manifest liest das Skript aus dem gebauten Paket (nicht jedes Plugin hat eine plugin.json im Quellbaum). Das Build-Skript eines freigegebenen Plugins muss OUTPUT_DIR aus der Umgebung beachten (OUTPUT_DIR="${OUTPUT_DIR:-$HOME/Desktop}"), sonst findet das Skript das Paket nicht.
  • Datenschutz-Scan. Vor jeder Änderung prüft registry-tools/privacy_scan.py die Texte, die Besucher lesen: name, description, changelog und system_prompt_usage der plugin.json, die beschreibung der Plugin-Actions (iris/Iris#149 — they stand in the consent and install dialog), store/description.md, custom.md, changelog.md, store-page.json und die Seiten der Anleitung (docs/) auf private IP-Adressen, Pfade im Home-Verzeichnis, E-Mail-Adressen, Zugangsdaten-Muster und interne Hostnamen (.local, .lan, .home, .internal), dazu jede Textdatei im Paket (also auch die runtime.lock) auf eindeutige Geheimnisse (Tokens, Schlüsselblöcke). Ein Fund lehnt das Plugin ab; die Meldung nennt Datei, Zeile und Art, nie den Fund selbst. Eigene Stichwörter (ein Wort je Zeile) liegen in $IRIS_PRIVATE_WORDS_FILE, sonst ~/.config/iris-private-words.txt, bewusst außerhalb des Repos. Die öffentliche Forgejo-Domain ist erlaubt.
  • Plugin-Aktionen (iris/Iris#149). A Plugin declaring actions in its Manifest gets checked before publication: registry-tools/plugin_actions.py (single source of truth, same rules as the app's PluginActionsCore.swift) validates actions, autonomous_block_types, launchers, runtime and platforms — a defective declaration is refused (registry_index.py add). The Lock-File of runtime.lock must lie in the package and every package line must be hash-pinned (--hash=sha256:); an unpinned dependency is refused. The Store-Seite generator (store_page.py) renders the Action-Beschreibungen from the Manifest in Klartext (section „Aktionen“) — the counterpart of the Installationsdialog in the App.
  • Version erhöhen. Dieselbe Version darf nur mit demselben Inhalt noch einmal laufen (dann „unverändert, übersprungen“). Wer ein Plugin ändert, erhöht version in plugin.json (und in build-irisplugin.sh, wenn sie dort auch steht), sonst lehnt das Skript mit „Die Version in plugin.json erhöhen“ ab. Eine ältere Version als die veröffentlichte wird ebenfalls abgelehnt.
  • Ein Fehler stoppt nicht die anderen. Schlägt der Build eines Plugins fehl, läuft der Rest weiter; am Ende steht die Liste der Fehlschläge, Exit-Code 1.
  • Kein Token im Skript oder im Klon. Pull und Push nutzen $FORGEJO_TOKEN als Git-Header, falls gesetzt, sonst die Zugangsdaten, die git ohnehin hat.
  • Plattformen: Ein Plugin kann sie im Manifest festlegen ("platforms": ["macos"], zum Beispiel ein lokaler Mac-Treiber ohne natives Framework); sonst gilt: nur mit native_framework_path ist es Mac-only.

Einmalig einrichten

  1. Das Repo iris/iris-plugin-registry ist angelegt (öffentlich, ohne PR-Zwang auf main, weil das Skript direkt pusht).
  2. Lokaler Klon: ~/Documents/2ndbrain/plugin-registry (ist vorhanden).
  3. ./publish-plugins.sh --all --dry-run, danach ./publish-plugins.sh --all — erst nach der Prüfung in PLUGIN-REGISTRY-REPO.md.

Tests

python3 -m unittest discover -s registry-tools (80 Tests für Index, Versionsregeln, Aufräumen, Prüfsummen, beschädigte Indexdateien, Kategorien, Abhängigkeiten, store/, Changelog und Anleitung). Das Skript selbst wurde gegen ein lokales Bare-Repo geprüft (Probelauf, echte Veröffentlichung, Wiederholung als „unverändert“, fehlender Klon, unbekanntes Plugin).


20. Marktplatz und Store: Plugins store-tauglich machen

Der Plugin-Marktplatz ist in den Apps (Iris, IrisClient, IrisRemote) über den Knopf „Marktplatz“ im Plugin-Manager zu erreichen. Er zeigt den Katalog der Registry (Kapitel 19) wie ein App Store: nach Kategorien, mit Suche, Icon, Langtext, Bildern und Videos. Installiert wird immer auf dem Server, mit dem die App verbunden ist (Mac oder Linux), nie auf dem Gerät des Clients. Dieses Kapitel sagt, was ein Plugin dafür mitbringen muss und wie man es prüft.

Was Nutzer sehen

  • Karte (Übersicht): Icon, Name, die ersten zwei Zeilen von description aus der plugin.json, Stand (installiert, Update); bei Plugins mit Anleitung oben rechts das Symbol „Anleitung“ (ein Buch). Der erste Satz der description muss also sagen, was das Plugin tut. Plugins, die auf dem Server nicht laufen, sind ausgegraut und nennen den Grund.
  • Detailseite: Icon, Name, Kategorie, Knöpfe, optional die Store-Seite (hero.html) oder, ohne sie, der Langtext (store/description.md, sonst die description), darunter die eigene Notiz (store/custom.md) mit Bildern und Videos, dazu Version, Größe, „Läuft auf“, „Braucht Iris“, „Braucht“ (requires) und „Wird gebraucht von“. Drei Knöpfe öffnen weitere Ansichten: „Versionen (N)“ (die Versionen der Registry, die letzten fünf, mit Datum, Größe und dem Änderungstext; eine ältere Version lässt sich installieren, der Dialog heißt dann „Zurücksetzen“ und warnt), „Changelog“ (store/changelog.md, nur wenn die Datei da ist) und „Anleitung“ (die Seiten aus user_docs, mit Auswahl nach Titel; nur wenn eine Anleitung da ist).
  • Ohne store/: Das Plugin erscheint trotzdem, mit einer Platzhalterkarte (Farbe und Symbol der Kategorie, Text aus der plugin.json). store/ ist also nie Pflicht, aber der Unterschied zwischen Platzhalter und fertiger Karte ist deutlich.
  • Bestätigungsdialog (vor jedem Installieren, Aktualisieren, Entfernen): Er zeigt nicht den Werbetext, sondern was im geprüften Paket steht: Zielserver und Version, fehlende Abhängigkeiten (werden zuerst mitinstalliert), die Skills (je SKILL.md und alle Dateien zum Aufklappen), den Systemprompt, Job-Vorlagen, die Installations-Skripte im Klartext (auf dem Mac-Server werden sie ausgeführt, auf dem Linux-Server nicht), die Python-Pakete, Einrichtungsfragen, eingebaute Hermes-Toolsets, die das Plugin abschaltet (disables_hermes_toolsets), und welches Treiber-Plugin ersetzt würde. Bei einem Update zusätzlich, was sich gegenüber der installierten Version geändert hat: der Änderungstext (changelog aus der plugin.json der neuen Version) als erster Punkt; hat das Plugin keinen, nennt der Dialog die geänderten Dateien (andere Dateien, neue, entfernte), dazu Hooks, Python-Pakete, Skills, Systemprompt und Abhängigkeiten. Beim Entfernen: welche installierten Plugins das Plugin brauchen, und ob die Einstellungen (<id>_plugin_config.json) mitgelöscht werden sollen (Standard: nein). Folge für Autoren: Hooks kurz und lesbar halten, nichts verschleiern, jeden Download im Hook so schreiben, dass man ihn dem Skript ansieht. Wer den Dialog liest, soll in einer Minute verstehen, was passiert.

Checkliste je Plugin

# Was Wo Pflicht
1 category — genau eine ID aus der Liste unten plugin.json ja, zum Veröffentlichen
2 description — ein bis zwei Sätze, der erste sagt, was es tut plugin.json ja
3 requires — Plugins, die dieses braucht (nur Belegtes, siehe unten) plugin.json nein
4 platforms — wenn es nur auf Mac oder Linux sinnvoll ist plugin.json nein, aber wichtig
5 store/icon.svg (oder .png/.webp) Quellbaum nein, empfohlen
6 store/description.md — Langtext Quellbaum nein, empfohlen
7 store/custom.md mit Bildern/Videos in store/_attachments/ — echte Aufnahmen, Erklärtext Quellbaum nein
8 store-page.json im Plugin-Ordner, daraus store/hero.html und das Icon erzeugen (registry-tools/store_page.py) Quellbaum nein, empfohlen
9 .include (Freigabe) und ein Build-Skript, das OUTPUT_DIR beachtet Plugin-Ordner zum Veröffentlichen
10 Version erhöhen (in plugin.json und, wo dort auch gesetzt, im Build-Skript) — bei geänderter plugin.json
11 changelog — was sich in dieser Version ändert, ein bis drei Sätze für Anwender plugin.json bei jedem Update empfohlen (iris-plugin-dev verlangt es)
12 store/changelog.md — ein Abschnitt ## <Version> (<Datum>) je Version, neueste zuerst Quellbaum nein, empfohlen
13 docs/erste-schritte.md (und weitere Seiten) mit user_docs in der plugin.json; das Build-Skript kopiert docs/*.md ins Paket Quellbaum, Paket nein, empfohlen

Kategorien

Die Liste steht in registry-tools/categories.json (ID, Name, Symbol, Farbe, Reihenfolge) und wird bei jedem Veröffentlichen in den Katalog übernommen. Ein Plugin hat genau eine; eine unbekannte ID lehnt das Veröffentlichen ab.

ID Name Wofür
media Medien und Kreativ Bild, Video, Audio, Musik, Sprache erzeugen oder bearbeiten, 3D, Spiele
knowledge Wissen und Dokumente Dokumente umwandeln, übersetzen, transkribieren, Tabellen, Notizen
automation Automatisierung und Alltag E-Mail, Rezepte, Web-Automatisierung, Bedienung von Iris selbst
development Entwicklung und Analyse Code, Reverse Engineering, Prüfwerkzeuge
infrastructure Infrastruktur Server, GPU-Hosts, Modelle, Treiber, Unterbau für andere Plugins
security Sicherheit Prüfen, Absichern, Zugangsdaten

Passt keine Kategorie, nicht raten: die Liste in categories.json erweitern (ein Eintrag, dann erscheint sie im Katalog), statt ein Plugin falsch einzuordnen.

requires und platforms

  • requires ist eine Liste von Plugin-IDs. Der Marktplatz zeigt sie, installiert fehlende Abhängigkeiten vor dem Plugin (in einem gemeinsamen Dialog) und warnt beim Entfernen, wenn andere Plugins darauf bauen. Jede genannte ID muss in der Registry stehen (sonst meldet registry_index.py check einen Fehler) und auf dem Server laufen (sonst ist das Plugin gesperrt, mit Grund). Nur eintragen, was belegt ist (das Plugin ruft den Skill oder Client des anderen wirklich auf), nicht, was nur „gut dazu passt“. Beispiel: comfyui-workflows → ["comfyui-bridge"].
  • platforms legt fest, wo ein Plugin läuft. Fehlt die Angabe, gilt es als überall lauffähig (außer mit native_framework_path). Braucht ein Plugin lokale Hardware oder Frameworks eines Systems, gehört es auf dessen Seite: MLX und Apple-Frameworks auf ["macos"], eine GPU, die der Server selbst hat, auf ["linux"]. Wer dagegen einen entfernten GPU-Host per SSH nutzt, läuft überall und braucht keine Angabe. Der Marktplatz sperrt ein Plugin auf der falschen Seite und nennt den Grund („Läuft nur auf dem Mac“).

Versionen, Änderungstext, Changelog und Anleitung

Vier Dinge sehen Nutzer im Marktplatz, bevor sie ein Plugin installieren oder aktualisieren. Alle vier schreibt der Autor, keines entsteht von selbst:

Was Wo Wann sichtbar Neue Version nötig?
Änderungstext der Version changelog in der plugin.json im Update-Dialog (statt der geänderten Dateien) und in der Versionsliste ja (das Manifest ist im Paket)
Changelog (alle Versionen) store/changelog.md Knopf „Changelog“ in der Detailansicht nein (Store-Material)
Anleitung docs/<name>.md und user_docs in der plugin.json Symbol „Anleitung“ in der Kachel, Knopf in der Detailansicht, im Plugin-Manager die Seitenliste ja, nur der Text der Seiten nein
Versionsliste history der Registry (die letzten fünf Versionen) Knopf „Versionen (N)“, ältere Version installieren (Zurücksetzen) — (entsteht beim Veröffentlichen)

Änderungstext schreiben: ein bis drei Sätze für Anwender, was sich in dieser Version ändert, keine Dateinamen und keine Commit-Sprache („Neu: …“, „Behoben: …“). Er wird bei jeder neuen Version ersetzt, nicht ergänzt. Fehlt er, zeigt der Dialog die geänderten Dateien. Bei einem Update über mehrere Versionen zeigt der Dialog nur den Text der Zielversion. store/changelog.md: erste Zeile # Changelog – <Name>, dann je Version ## <Version> (<Datum>) und darunter derselbe Text, neueste zuerst. Markdown nur in der kleinen Teilmenge von store/custom.md, ohne Bilder. iris-plugin-dev schreibt Manifest-Text und Abschnitt zusammen (changelog --text "…") und prüft sie. Anleitung schreiben: mindestens docs/erste-schritte.md mit Was es tut, Einrichten, Benutzung im Chat, Grenzen; nur Aussagen, die Manifest, Skill oder Einstellungsfenster belegen. Weitere Seiten (zum Beispiel „Befehle“, „Grenzen und Fehlerbehebung“) stehen in user_docs in der gewünschten Reihenfolge. Bilder und Links gibt es in der Anleitung nicht. Das Build-Skript muss docs/*.md ins Paket kopieren (der Plugin-Manager zeigt sie von dort); publish-plugins.sh liest docs/ aus dem Quellbaum.

Für Clients (Apps, Server) stehen die Felder im Wire-Protokoll: versions, changelog, docs im Katalog, target_version an der Brücke, files_changed und Verwandte im Update-Vergleich (Entwicklerdokumentation/02-Wire-Protokoll.md).

Das Verzeichnis store/

Liegt im Plugin-Ordner im Quellbaum, wird nicht ins Paket gepackt und kommt beim Veröffentlichen nach store/<id>/ in die Registry. Geänderte Medien lassen sich auch ohne neue Version veröffentlichen („nur store/ aktualisiert“). Erlaubt ist genau:

Datei Inhalt Grenze
icon.svg / icon.png / icon.webp ein Icon (nur eines) 512 KB
description.md Langtext 64 KB
hero.html eigene Kopfseite oder Animation 64 KB
screenshots/<datei> Bilder (png, jpg, webp, gif) und Videos (mp4, webm), Namen aus a-z A-Z 0-9 . _ -, sortiert nach Name höchstens 12 Dateien, je 8 MB
custom.md eigene Notiz in Markdown, erscheint in der Detailansicht unter der Store-Seite (siehe „Eigene Notiz“) 64 KB
changelog.md vollständige Changelog in Markdown (ohne Bilder), ein Abschnitt ## <Version> (<Datum>) je Version, neueste zuerst, ein bis drei Sätze für Anwender; der Marktplatz zeigt sie über den Knopf „Changelog“ in der Detailansicht (Index: store.changelog). Store-Material: geht ohne neue Version in die Registry 64 KB
docs/ des Plugins (kein Store-Ordner) Anleitung: Die Seiten aus user_docs der plugin.json (docs/<name>.md, Reihenfolge wie dort) rendert publish-plugins.sh zu store/<id>/anleitung/<name>.html; der Marktplatz zeigt sie über das Symbol „Anleitung“ in der Kachel und der Detailansicht (Index: store.docs). Gleiche Seiten wie im Plugin-Manager, kein Bild, kein Link; geht ohne neue Version in die Registry je Seite 64 KB
_attachments/<datei> Bilder und Videos der Notiz (nur die dort verwendeten werden veröffentlicht) je 8 MB

Alles zusammen höchstens 25 MB je Plugin. Andere Dateien, Symlinks und Unterordner außer screenshots/ und _attachments/ lehnt das Veröffentlichen mit der Dateinamen-Meldung ab; versteckte Dateien (Namen mit . am Anfang, etwa .DS_Store) werden ignoriert. Gebraucht wird store/ nur beim Veröffentlichen; die Server liefern die Medien aus der Registry aus und prüfen sie gegen die Prüfsumme im Katalog.

Eigene Notiz (custom.md)

Der einfachste Weg zu Bildern, Videos und Erklärtext: eine ganz normale Obsidian-Notiz als store/custom.md, die Medien daneben in store/_attachments/ — genau wie beim Benutzerhandbuch. Der Marktplatz zeigt sie in der Detailansicht unter der Store-Seite (hero.html). Beim Veröffentlichen wird sie zu HTML (registry-tools/store_markdown.py); die Registry enthält nur store/<id>/custom.html und die verwendeten Dateien aus _attachments/ (Index: store.custom). Nicht verwendete Dateien in _attachments/ bleiben lokal.

  • Bilder und Videos: ![[Bild.png]], ![[Film.mp4]] oder ![Text](_attachments/Bild.png). Erlaubt: png, jpg, webp, gif, mp4, webm; Namen aus a-z A-Z 0-9 . _ - Leerzeichen; höchstens 8 MB je Datei, mit allem anderen zusammen 25 MB je Plugin. Eine verwendete Datei, die fehlt, lehnt das Veröffentlichen mit dem Namen ab.
  • Text: #/##/### (kleine Zwischenüberschriften), Absätze, Listen (auch verschachtelt, nummeriert), Zitate, Codeblöcke, ---, einfache Tabellen, **fett**, *kursiv*, `Code`.
  • Roh-HTML wird als Text gezeigt, Links und Bilder von anderswo nur als Text (ein Klick darf den Marktplatz nicht verlassen). Ein Klick auf ein Bild vergrößert es.
  • Wie bei allen Medien: Die Registry ist öffentlich und bleibt im Git-Verlauf — keine privaten Daten (Adressen, Namen, Mailkonten, Pfade, Chats) in Bildern oder Text. Bilder vorher verkleinern (rund 1600 px, unter 500 KB).

Icon

  • Quadratisch, Quellgröße mindestens 128 × 128 (bei SVG viewBox="0 0 128 128"), Hintergrund randvoll: Die App rundet die Ecken selbst (nicht vorab abrunden).
  • Ein einfaches Symbol, kein Text — das Icon erscheint ab 56 px Kantenlänge. Hintergrund in der Farbe der Kategorie (Farben aus categories.json), Symbol hell und mittig; innerhalb einer Kategorie dieselbe Grundfarbe, damit der Katalog ruhig wirkt.
  • SVG ohne Skripte, Schriften und externe Verweise, klein (wenige KB). SVG aus dem Store läuft ohnehin abgeschottet, aber ein sauberes Icon braucht nichts davon.

description.md

Der Langtext ist deutsch, sachlich und kurz (etwa 100 bis 250 Wörter). Gezeigt wird eine einfache Markdown-Teilmenge:

Erlaubt Anzeige
#, ##, ### Überschriften kleine Zwischenüberschriften
- Punkt / * Punkt Liste
leere Zeile neuer Absatz
`Code`, **fett** Code, fett

Nicht unterstützt (erscheint als Text oder gar nicht): Links, Bilder, Tabellen, nummerierte Listen, HTML, Zitate. Bilder und Tabellen gehören in die eigene Notiz custom.md, die mehr kann (siehe „Eigene Notiz“).

Gliederung, die sich bewährt hat:

## Was es kann
Zwei, drei Sätze: der Nutzen, nicht die Technik.

## Was du dafür brauchst
- Voraussetzungen: andere Plugins, GPU-Host, Konten, Plattform

## Gut zu wissen
- Grenzen, was installiert wird (Skripte, Pakete), wo Einstellungen liegen

Nur aufschreiben, was das Plugin wirklich tut (die SKILL.md und das Manifest sind die Quelle). Keine Versprechen, keine Werbesprache, keine Versionsnummern und Daten (die stehen im Katalog).

Store-Seite: store-page.json und store_page.py

Jedes Plugin kann eine eigene Seite haben, die die Detailansicht im Marktplatz anstelle des schlichten Textes zeigt: ein Kopfbereich in der Kategorie-Farbe, „So funktioniert’s“, ein Ablaufdiagramm, Funktionskarten, Beispielsätze für den Chat, „Das brauchst du“ und „Gut zu wissen“. Hell und dunkel, auf dem Telefon einspaltig. Das Layout ist für alle Plugins gleich; du schreibst nur den Inhalt, ein Skript erzeugt die Seite:

registry-tools/store_page.py <plugin-id>        # schreibt store/hero.html und store/icon.svg
registry-tools/store_page.py --all              # für alle Plugins mit store-page.json
registry-tools/store_page.py --check --all      # prüft nur, ob die Dateien zum Inhalt passen (Exit 1 sonst)

store-page.json liegt im Plugin-Ordner (nicht in store/, nicht im Paket). publish-plugins.sh bricht für ein Plugin ab, dessen store/hero.html nicht zu seiner store-page.json passt.

Feld Inhalt
glyph Symbol des Plugins, für Icon und Seite (Namen in registry-tools/store_glyphs.py; neue Symbole nur dort ergänzen)
tagline ein Satz unter dem Namen: was tut das Plugin
intro optional, ein bis zwei Sätze
badges optional, bis 5 Kennzeichen („Mac und Linux“, „GPU-Host nötig“)
steps 2 bis 4 × {title, text}: „So funktioniert’s“
flow optional, 2 bis 5 × {glyph, label, note?}: der Weg als Diagramm (zum Beispiel Du im Chat → Iris → GPU-Host → Text)
features 2 bis 8 × {glyph, title, text}: „Das kann es“
examples optional, bis 5 Sätze, die man im Chat sagen kann. Aus dem SKILL.md des Plugins nehmen (die Auslöser-Sätze), nicht erfinden
needs, notes optional, bis je 8 Zeilen: „Das brauchst du“, „Gut zu wissen“

A Plugin declaring actions in its Manifest gets an extra section „Aktionen“ automatically: store_page.py reads the beschreibung of every action from plugin.json and shows them in Klartext — the same texts the Installationsdialog in the App shows (iris/Iris#149). Nothing to write in store-page.json for them.

Texte sind höchstens 320 Zeichen lang; erlaubt sind **fett** und `Code`, alles andere wird als Text dargestellt (kein HTML, keine Links). Die Seite ist in sich geschlossen (kein Netzwerk, keine Schriften) und etwa 10 bis 15 KB groß; sie meldet dem Marktplatz ihre Höhe selbst. Zum Ansehen im Browser: ?theme=light oder ?theme=dark an die Adresse hängen. Gleiche Regeln wie überall: nur Belegtes schreiben, nichts versprechen.

Screenshots, Videos, Hero

  • Nur echte. Keine nachgebauten oder bearbeiteten Bilder, die etwas zeigen, was das Plugin nicht kann. Die Reihenfolge ergibt sich aus der Notiz (custom.md); der Ordner screenshots/ (Bilderstreifen, Reihenfolge nach Dateiname 01-, 02-) geht weiterhin, ist aber nicht mehr nötig.
  • Bilder in der Notiz werden in voller Breite gezeigt (Klick vergrößert); Querformat (zum Beispiel 16:10) oder Hochformat funktionieren. Videos kurz halten (unter 8 MB), mp4 oder webm, ohne Ton nötig.
  • hero.html ist die Store-Seite (oben, normalerweise aus store-page.json erzeugt; von Hand nur, wenn die Vorlage nicht reicht). Sie ist eine eigene, in sich geschlossene HTML-Seite (inline CSS und JavaScript, keine externen Dateien und kein Netzwerk). Sie läuft in einem abgeschotteten Rahmen ohne Zugriff auf die Seite, das Lese-Token oder den Server; sie kann also nichts nachladen. Nur verwenden, wenn eine Animation mehr zeigt als ein Bild.

Prüfen, bevor man veröffentlicht

./publish-plugins.sh --list                     # zeigt je Plugin  [Kategorie, store]; fehlt die Kategorie, steht dort "-"
./publish-plugins.sh <plugin-id> --dry-run      # baut, prüft Kategorie, requires und store/ gegen die Regeln, ändert die Registry nicht
python3 -m unittest discover -s registry-tools  # Tests der Index-Regeln

Der Probelauf nennt Verstöße mit Dateinamen („store/notes.txt: unerwartet“, „… zu groß“, „category fehlt oder ist unbekannt“, „Das Paket enthält store/“, „Ein Plugin mit Konfigurationsfenster braucht den Befehl „config-status““). Ein Plugin mit geänderter plugin.json und gleicher Version lehnt er ab („Die Version in plugin.json erhöhen“).

Wie es später aussieht, ohne die echte Registry anzufassen: in ein leeres Test-Repo veröffentlichen und einen Server darauf zeigen lassen.

mkdir /tmp/testregistry && git -C /tmp/testregistry init
./publish-plugins.sh <plugin-id> --registry-dir /tmp/testregistry --no-pull --no-push
(cd /tmp/testregistry && python3 -m http.server 8099)       # in einem zweiten Terminal
IRIS_PLUGIN_REGISTRY_URL=http://127.0.0.1:8099 …             # Umgebungsvariable des Servers (Mac: auch UserDefaults `pluginRegistryURL`)

Veröffentlichen

  1. plugin.json fertig (Kategorie, requires, platforms, bei Konfigurationsfenster config-status in panel_api.commands, bei Rechner-Bedarf host_capabilities/local_mode), store/ gefüllt, Version erhöht.
  2. ./publish-plugins.sh <plugin-id> --dry-run, dann ./publish-plugins.sh <plugin-id> (oder --all). Das Skript baut, prüft, trägt in index.json ein, committet und pusht die Registry (Kapitel 19). Veröffentlicht wird von Hand, nie automatisch.
  3. Nur Plugins mit .include erscheinen im Marktplatz; alle anderen bleiben privat.
  4. Nur das Material geändert? Dieselbe Version genügt: das Skript meldet „nur store/ aktualisiert“.

21. Rechner, Rechnerwahl und Konfigurations-Zustand

Ein Plugin, das einen anderen Rechner braucht (GPU-Karte, Docker), trägt nie SSH-Adresse oder Schlüssel in seiner Konfiguration. Das Plugin iris-server-hosts („Hosts“) führt die Rechner an einer Stelle (Adresse, gemeinsamer SSH-Schlüssel, was ein Rechner kann, Einrichtung frischer Rechner); jedes andere Plugin wählt in seinem Konfigurationsfenster nur einen Rechner aus der Liste und speichert dessen Kennung (host_id). Alle Routen und Regeln sind auf Mac- und Linux-Server gleich (Wire-Protokoll: Entwicklerdokumentation/02-Wire-Protokoll.md); der Marktplatz zeigt den Zustand jedes Plugins am Knopf „Konfigurieren“.

21.1 Rechner im Manifest: host_capabilities und local_mode

{
  "id": "mein-gpu-plugin",
  "host_capabilities": ["gpu"],
  "local_mode": ["macos"],
  "has_settings_panel": true,
  "settings_web_view_path": "web/settings.html",
  "panel_api": { "script": "python/panel_api.py", "commands": ["config-status"], "timeout": 30 }
}
Feld Bedeutung
host_capabilities Anforderungen an den Rechner: gpu (eine NVIDIA-Karte wurde erkannt), docker, os (Betriebssystem wurde gelesen). Eine unbekannte Anforderung erfüllt kein Rechner. Die Fähigkeiten liest die Sonde beim Prüfen (ein SSH-Aufruf: nvidia-smi, docker --version, /etc/os-release) und speichert sie im Host-Eintrag.
local_mode Das Plugin hat einen lokalen Weg auf dem Iris-Server: true oder eine Plattformliste (macos, linux). Beispiel: Marker und Whisper laufen auf dem Mac lokal auf der Apple-GPU, ein Linux-Server braucht dagegen einen NVIDIA-Rechner.

Zusammenspiel:

Plugin Felder Sperre Ohne passenden Rechner Picker
Marker, Whisper (Mac) host_capabilities + local_mode blockiert nie lokal installiert (host_mode: "local") passende Rechner + „Dieser Iris-Server (lokal)“
HeartMuLa nur host_capabilities blockiert ohne passenden Rechner nicht installierbar nur passende Rechner
ohne Rechner-Bezug keines prüft nichts normal gibt es nicht

Die Installations-Sperre lässt ein Plugin nur zu, wenn mindestens ein Rechner alle Anforderungen erfüllt (zwei Rechner, einer mit Karte, einer mit Docker, erfüllen zusammen kein Plugin, das beides verlangt). Dieselbe reine Funktion gibt es dreimal und sie gleichen sich gegenseitig ab: Plugin-Kern (iris-server-hosts/python/iris_server_hosts_core.py), Linux-Server (hosts_targets.py), Mac (IrisHostCapabilities.swift).

21.2 Der Host-Picker: GET /hosts/targets

Das Panel fragt GET /hosts/targets?token=<cfg_token>&needs=gpu&needs=docker und bekommt {"targets": [{"id", "name", "ssh_target", "gpus": [{"name", "vram_mib"}], "docker", "os"}]}. Gefiltert wird nach host_capabilities des Manifests; needs (wiederholter Parameter) ergänzt das für den einzelnen Aufruf, lockert es nie. Der Eintrag self („Dieser Iris-Server“) ist immer ein Host; bei einem Plugin mit local_mode steht er auch dann in der Liste, wenn der Rechner die Anforderungen nicht erfüllt (das ist dann der lokale Weg). Ein Panel zeigt target.gpus (Name, VRAM) als Beschriftung und speichert target.id.

Konfigurationsfenster, Konvention (siehe Marker, HeartMuLa, Whisper): Abschnitt „Installieren auf:“ mit dem Picker; ohne gespeicherte Wahl steht „Bitte wählen …“ (der Zustand bleibt Gelb, bis gewählt und gespeichert ist); darunter nur, was zur Wahl gehört (lokal einrichten oder auf dem Rechner installieren, „Optionen“ für Pfad und Ähnliches); keine Felder für SSH-Ziel, Schlüssel oder Server-Adresse (auch nicht unter „Erweitert“); läuft das Plugin auf dem Rechner schon, steht das grün als Zustand („installiert“, Knopf „Aktualisieren“), Rot nur, wenn eine ausdrücklich gestartete Aktion scheitert. Gespeichert wird host_id (plus Plugin-Eigenes wie install_path); ein altes ssh_target dient höchstens als Vorauswahl und entfällt beim ersten Speichern.

21.3 Laufzeit: GET /hosts/resolve (Kennung → SSH-Ziel)

Plugin-Skripte (zum Beispiel heartmula-cli, whisper-cli, skills/videowatch) brauchen zur Laufzeit das SSH-Ziel. Sie lesen nie die Host-Liste und fragen den Server: GET /hosts/resolve?host_id=<Kennung> antwortet {"ssh_target": "benutzer@adresse"} oder 404 bei unbekannter Kennung (nie eine Adresse als Kennung). Zugang: auf dem Linux-Server Bearer (Host, Port und Token stehen in iris_notify_config.json, ein Skript in einer Sandbox erreicht den Server über die Bridge-Gateway-Adresse, siehe determine_iris_endpoint in heartmula-cli); auf dem Mac nur von dieser Maschine (Loopback), ohne Bearer, wie /notify. Wer das Feld ssh_target einer fremden Konfiguration gelesen hat (videowatch las das der Whisper-Konfiguration), muss vor der Umstellung gefunden und angepasst werden: vorher im ganzen Repo nach dem Namen der Konfigurationsdatei suchen.

Der lokale Weg auf dem Mac braucht kein SSH: Für die Kennung self führt whisper-cli seine Befehle mit dem Ziel local direkt aus (gleiche Befehlszeichenfolge, gleiche Dateien, nur ohne Verbindung); Marker richtet seine lokale Umgebung im Hook ein.

21.4 Installation bei „Laden“: host_mode und IRIS_HOST_MODE

Beim Klick auf „Laden“ im Marktplatz prüft der Server die Rechner (Schritt 1b in Kapitel 9) und meldet im Ergebnis von install das Feld host_mode:

host_mode Bedeutung Was passiert
fehlt das Plugin verlangt keinen Rechner normale Installation
"local" kein passender Rechner, das Plugin hat einen lokalen Weg lokal einrichten; der Hook bekommt IRIS_HOST_MODE=local
"pending" mindestens ein passender Rechner, auch bei genau einem der Hook richtet nichts ein (IRIS_HOST_MODE=pending), die App öffnet das Konfigurationsfenster mit dem Picker

Der Linux-Server führt keine Hooks aus; er meldet nur host_mode im Ergebnis. Ein Hook muss pending als Erfolg beenden (exit 0) und darf auf einem Rechner, der lokal nichts kann (Intel-Mac, Linux), nicht abbrechen: der Zustand zeigt dann Gelb. Frühere install_config-Fragen nach einem Rechner oder einer Server-Adresse entfallen (der Marktplatz kann Plugins mit Pflicht-Fragen nicht installieren).

21.5 Zustand für den Marktplatz: config-status

Jedes Plugin mit Konfigurationsfenster führt in panel_api.commands den Befehl config-status (Pflicht beim Veröffentlichen). Das Skript gibt genau ein JSON-Objekt aus:

{"state": "configured", "message": "HeartMuLa ist auf dem gewählten Rechner installiert"}
{"state": "needs_configuration", "message": "Noch kein Rechner gewählt: im Konfigurationsfenster einen Rechner wählen."}
state Farbe Bedeutung
configured grün das Plugin funktioniert so, wie es jetzt steht
needs_configuration gelb es funktioniert so noch nicht; message nennt, was fehlt
(vom Server erzeugt) error rot die Prüfung selbst ist fehlgeschlagen: das Skript wirft, Zeitüberschreitung (10 s), der Befehl fehlt, keine JSON-Antwort, ungültiger Zustand. Der Fehlertext steht in der Kachel, nichts passiert still.

Regeln für das Skript: Gelb nur, wenn das Plugin so, wie es steht, nicht funktioniert (Marker lokal auf dem Mac ist Grün, ohne dass etwas gespeichert wurde); kein SSH und kein Netzwerk beim Öffnen des Marktplatzes (ob ein Rechner gerade antwortet, gehört in einen eigenen Check, nicht hierher); schnell (Standardbibliothek, wenige Dateien lesen). Ein Plugin merkt sich dafür selbst, was es festgestellt hat (HeartMuLa und Whisper speichern installed_on, die Kennung des Rechners, auf dem die Installation festgestellt wurde).

Wie die Antwort ankommt: GET /plugins/<id>/config-status (Bearer oder cfg_token) und für die Marktplatz-Seite GET /marketplace/api/plugins/<id>/config-status (Lese-Token); beide antworten immer mit HTTP 200 und {state, message}, nur ein nicht installiertes Plugin oder eines ohne Konfigurationsfenster ergibt 404. Die Seite fragt beim Öffnen alle installierten konfigurierbaren Plugins gleichzeitig ab, nach dem Schließen eines Konfigurationsfensters nur das betroffene („Neu prüfen“ für alle). Der Katalog trägt dafür das Feld configurable. Der Knopf „Konfigurieren“ steht in jeder Kachel eines installierten Plugins mit Panel und in Einstellungen → Plugins (Mac-App, Remote-Verwaltung und iOS-Liste); ein Klick öffnet das Panel über die Brücke irisMarketplace (Aktion configure).

21.6 Checkliste für ein Plugin mit Rechner-Bedarf

  1. Manifest: host_capabilities (und local_mode, wenn es lokal läuft), panel_api mit config-status.
  2. Panel: „Installieren auf:“ mit Picker (/hosts/targets), nur host_id speichern, keine SSH-Felder, „Bitte wählen …“ ohne Wahl.
  3. Laufzeit-Skripte: Kennung über GET /hosts/resolve auflösen (nicht ssh_target lesen); lokalen Weg ohne SSH lösen.
  4. config-status schreiben und testen (Gelb ohne Wahl, Grün nur mit Feststellung, Standardbibliothek, kein SSH).
  5. Alle Leser der Konfigurationsdatei des Plugins suchen (andere Skills, Tools) und anpassen.
  6. Veröffentlichen: ./publish-plugins.sh <id> --dry-run prüft config-status; danach im Marktplatz installieren und den Knopf in der Kachel prüfen.

Referenz-Umsetzungen: marker (lokal + Rechner, Hook mit IRIS_HOST_MODE, lokale Einrichtung über das Panel), heartmula und whisper (Rechner per Resolver, whisper mit lokalem Ziel), iris-server-hosts (die Liste selbst und config-status ohne Rechner-Bezug). Die Entscheidungen und der Verlauf: Issues iris/iris-plugins#115, #135, #138, #140 und iris/Iris#85.


22. Seitenpanel und Chat: wie ein Plugin mit dem Chat spricht

Ein Plugin mit side_panel im Manifest bekommt einen Reiter im rechten Seitenpanel (neben Notiz und Browser). Die Weboberfläche des Reiters ist an einen Chat gebunden: Nutzer und Agent arbeiten am selben Stand. Beispiele: comfyui-workflows (Workflow-Bibliothek), iris-solidcraft (Designs des Chats) und iris-gamebox (Spiele-Katalog).

Dieses Kapitel erklärt das Zusammenspiel aus Sicht des Plugin-Autors. Der Vertrag selbst — Routen, Felder, Zeitlimits — steht nur im Wire-Protokoll (Entwicklerdokumentation/02-Wire-Protokoll.md, Abschnitt „Plugin-Reiter im Seitenpanel“); hier wird er nicht wiederholt, nur erklärt. Plan und Entscheidungen: Issue iris/Iris#169.

22.1 Wer redet mit wem

flowchart LR
    N["Nutzer"]
    subgraph Client["Client: Iris, IrisClient oder IrisRemote"]
        TAB["Reiter im Seitenpanel"]
        CHAT["Chat-Eingabe"]
    end
    subgraph Server["Server: Mac-Iris oder Linux-Server"]
        ROUTE["Route /plugins/ID/api/BEFEHL"]
        AGENT["Agent Hermes"]
    end
    subgraph Plugin["Plugin im Paket"]
        PAGE["web/seite.html mit panel_kit.js"]
        API["panel_api.py mit panel_kit.py"]
        SKILL["Skill mit eigener CLI"]
        STATE[("Zustand: selection.json und Plugin-Daten")]
    end

    N --> TAB
    N --> CHAT
    TAB -- "zeigt" --> PAGE
    PAGE -- "api: Befehl plus chat_id" --> ROUTE
    TAB -- "fragt status etwa alle 5 s" --> ROUTE
    ROUTE -- "startet Skript, Body auf stdin" --> API
    API <--> STATE
    CHAT -- "Nachricht mit Zeile PLUGIN-PANEL" --> AGENT
    AGENT -- "ruft Befehle des Skills" --> SKILL
    SKILL <--> STATE

Drei Dinge folgen daraus:

  1. Die Seite spricht nie direkt mit dem Chat. Sie redet mit panel_api — über dieselbe generische Route wie jedes Konfigurationsfenster (Kapitel 15). Nicht mit der irisCall-Brücke der Canvas-Blöcke: Die führt Skripte auf dem Client-Gerät aus und existiert auf iOS nicht.
  2. Der Chat erfährt den Stand über den Client, nicht über den Server. Der Server kennt keinen Chat und kein Antwortformat; er vergibt nur den Token.
  3. Gemeinsamer Zustand liegt im Plugin, nicht im Chat: panel_api.py (für den Reiter) und der Skill (für den Agenten) lesen und schreiben dieselben Dateien. Wer die Dateien an einer dritten Stelle schreibt, erzeugt Inkonsistenzen.

22.2 Ablauf: vom Öffnen des Reiters bis zur Antwort des Agenten

sequenceDiagram
    autonumber
    actor N as Nutzer
    participant C as Client mit Reiter
    participant S as Server
    participant P as panel_api.py
    participant W as Seite im Reiter
    participant A as Agent

    N->>C: öffnet den Reiter
    C->>S: GET side-panel-webview-url
    S-->>C: cfg_token und Pfad der Seite
    C->>W: lädt Seite mit cfg_token und chat_id in der URL

    loop solange der Reiter offen ist
        C->>S: POST api/status mit chat_id
        S->>P: python3 panel_api.py status
        P-->>S: ok, chat_id, summary, detail
        S-->>C: Antwort
        Note over C: summary und chat_id werden gemerkt
    end

    N->>W: wählt etwas aus
    W->>S: POST api/select mit Auswahl und chat_id
    S->>P: python3 panel_api.py select
    P-->>W: neuer Stand wie bei status

    N->>C: tippt eine Nachricht und sendet
    C->>A: PLUGIN-PANEL Name summary, dann der Text des Nutzers
    A->>A: liest die Zeile und den Skill des Plugins
    A->>P: holt den vollen Stand über Befehle des Skills
    A-->>N: Antwort, die zum Stand passt

Wichtig für das Schreiben des Plugins:

  • status ist der einzige Pflichtbefehl. Er antwortet mit {ok, chat_id, summary, detail}. summary ist eine Zeile (der Client kappt hart auf 300 Zeichen, das Kit bleibt bei 240) und geht in jede Nachricht; detail geht nie automatisch in eine Nachricht. Den vollen Stand holt der Agent über Befehle seines Skills — die SKILL.md erklärt ihm welche.
  • status ist billig. Der Reiter fragt etwa alle 5 Sekunden, das Zeitlimit liegt bei 10 Sekunden, und Senden wartet nie auf ein Plugin-Skript. Schwere Daten (große Indizes, Netzwerk) gehören nicht hinein — zählen aus kleinen Dateien, nicht den Vollindex laden.
  • chat_id kommt als Quittung zurück. Die Seite schickt sie bei jedem Aufruf mit; fehlt sie in der Antwort von status oder weicht sie ab, zeigt der Reiter einen Fehler statt fremder Daten. Sie ist kein Sicherheitsbeweis — der cfg_token begrenzt auf das Plugin, nicht auf den Chat. Nur wer sie in einen Pfad setzt (Chat-Workspace), prüft sie vorher (safe_chat_id).
  • Die gemeinsame Auswahl (select/selection) ist das Muster für „der Agent sieht, was der Nutzer angeschaut hat“: Die Seite ruft select, panel_api schreibt selection.json, status nennt die Auswahl vorn in der summary, der Agent liest sie im Marker oder per selection. Sie ist Bedienzustand, kein zweiter Katalog: nur etwas auswählbar machen, das es gibt.

22.3 Wie der Reiter eine status-Antwort auslegt

Die Auslegung liegt allein im Client-Kern PluginSidePanelCore.swift (interpretStatus); die Server kennen weder den Chat noch das Antwortformat.

stateDiagram-v2
    [*] --> Geschlossen
    Geschlossen --> Fragt: Nutzer öffnet den Reiter
    Fragt --> Gueltig: ok, chat_id stimmt
    Fragt --> FalscherChat: chat_id weicht ab
    Fragt --> Ungueltig: ok false, chat_id fehlt, keine Antwort oder mehr als 10 s
    Gueltig --> Fragt: nach 5 s
    FalscherChat --> Fragt: nach 5 s
    Ungueltig --> Fragt: nach 5 s
    Fragt --> Geschlossen: Nutzer schließt den Reiter
    Gueltig --> Geschlossen: Nutzer schließt den Reiter
    FalscherChat --> Geschlossen: Nutzer schließt den Reiter
    Ungueltig --> Geschlossen: Nutzer schließt den Reiter
    Geschlossen --> [*]
Ergebnis Auslöser Was der Reiter tut
gültig ok nicht false, chat_id ist die gesendete merkt summary (auf eine Zeile und 300 Zeichen gekappt) für den nächsten Marker
falscher Chat chat_id der Antwort weicht ab zeigt einen Fehler statt fremder Daten
ungültig {ok: false, error}, chat_id fehlt, keine JSON-Antwort, Zeitüberschreitung zeigt error bzw. den Grund

Solange der Reiter geschlossen ist, fragt niemand status, und es steht keine PLUGIN-PANEL-Zeile in der Nachricht. Ist der Reiter offen, aber noch ohne gültigen Stand, lautet die Zeile „geöffnet, ohne Stand“. Der Fehlertext des Plugins erreicht den Nutzer nur, wenn er als {ok: false, error} ankommt (Abschnitt 22.6) — bei einem Absturz ohne Text bleibt dem Reiter nur ein allgemeiner Grund („hat nicht geantwortet“).

22.4 Ansicht aktualisieren: wenn Iris etwas ändert

Ohne eigenes Zutun zeigt die Seite nur den Stand vom Laden. Damit sie Änderungen des Agenten zeigt (Spiel gebaut, Workflow registriert, Design gespeichert), gibt es einen kleinen zweiten Vertrag (iris/Iris#178, Einzelheiten im Wire-Protokoll): status meldet optional eine revision, der Client vergleicht sie und schickt der Seite bei einer Änderung das Ereignis iris:panel-refresh — ohne Neuladen. Die Seite holt dann nur ihre Daten neu und behält Suche, Filter, Scroll und das offene Element.

sequenceDiagram
    autonumber
    participant A as Agent
    participant P as panel_api.py
    participant C as Client mit Reiter
    participant W as Seite im Reiter

    A->>P: ändert den Stand über Befehle des Skills
    Note over A,C: Werkzeug fertig oder Lauf zu Ende
    C->>P: status sofort, nicht erst nach 5 s
    P-->>C: ok, chat_id, summary, revision neu
    C->>C: revision verglichen: geändert
    C->>W: Ereignis iris:panel-refresh mit revision und Anlass agent
    W->>P: panel.onRefresh holt Listen und Detail neu
    P-->>W: neuer Stand
    Note over W: Suche, Filter und Scroll bleiben
    Note over C,W: Knopf im Reiter: Anlass manual, auch ohne neue revision
Wer Was
Plugin (panel_api.py) revision=pk.revision_of(dateien…, extra=…) in status_reply — billig: Größe und Änderungszeit der Zustandsdateien, eine Ebene eines Ordners, kleine Zähler. Nie ein Vollindex: status läuft im 5-Sekunden-Takt. Sie ändert sich genau dann, wenn sich ändert, was die Seite zeigt.
Seite panel.onRefresh(async (info) => …): Daten neu holen, nicht neu aufbauen. Ohne Handler lädt das Kit die Seite hart neu — richtig für eine einfache Seite, falsch für eine mit Zustand. info.reason ist status, agent oder manual.
Client vergleicht revision je Seite (der erste Wert nach dem Laden ist keine Änderung), fragt status sofort nach jedem fertigen Werkzeugaufruf und am Ende eines Laufs (iOS nur am Ende), schickt das Ereignis, und lädt bei manual hart neu, wenn die Seite das Ereignis nicht kennt

Das Plugin entscheidet, was „sichtbar“ heißt: iris-gamebox nimmt Auswahl, Quellen, Konfiguration, die Bau-Jobs und die Ablage der fertigen Apps; comfyui-workflows Registry, Auswahl und Tag-Katalog; iris-solidcraft die Designs des Chats und den App-Zustand. Ein Plugin ohne revision bekommt keine automatische Aktualisierung — der Knopf wirkt trotzdem.

22.5 Ansichtszustand: Iris steuert, was der Reiter zeigt

Die gemeinsame Auswahl (22.2) trägt „welches Element“. Für „zeig mir Space Quest“ braucht der Agent mehr: Suche, Filter, Modus. Dafür hat ein Plugin einen Ansichtszustand (iris/iris-plugins#284) — eine kleine Datei im Plugin-Zustand (view.json, global wie die Auswahl: zwei offene Reiter zeigen denselben Stand), die beide Seiten schreiben: der Agent über die CLI seines Skills, die Seite über panel_api (view-set) für das, was der Nutzer selbst eingibt. Kein Klicken im DOM, keine Bildanalyse: der Agent ändert den Zustand, die Seite folgt über das Refresh-Ereignis (22.4).

sequenceDiagram
    autonumber
    actor N as Nutzer
    participant A as Agent
    participant P as Plugin mit view.json
    participant C as Client mit Reiter
    participant W as Seite im Reiter

    N->>A: Zeig mir Space Quest
    A->>P: gbox view --query Space Quest
    P->>P: Katalog durchsucht, erster Treffer gewählt, hit 1 von 3 gemerkt
    P-->>A: hit 1, hits 3, selection
    C->>P: status sofort nach dem Werkzeug
    P-->>C: summary, neue revision
    C->>W: Ereignis iris:panel-refresh
    W->>P: view lesen
    W->>W: Suchfeld gefüllt, Liste geladen, Treffer 1 gezeigt
    N->>A: nächster
    A->>P: gbox view --next
    P-->>A: hit 2
    C->>W: Ereignis, Seite zeigt Treffer 2
    N->>W: tippt selbst eine andere Suche
    W->>P: view-set mit Suche, hits, hit
    A->>P: gbox view liest, was der Nutzer sieht
Baustein Aufgabe
pk.ViewState(pfad, defaults, choices=, max_len=) die Datei: load() (Standardwerte, kaputte Einzelwerte fallen zurück), update(**felder) (prüft Feld, Auswahlliste, Länge; schreibt atomar; unbekanntes Feld = Fehler), clear()
CLI/Befehl view (Agent und Seite) Felder setzen ⇒ erster Treffer wird gezeigt; step (--next/--prev) und hit N (1-basiert) schalten durch dieselbe Liste, die die Seite zeigt (eine Trefferlogik, kein zweiter Algorithmus); am Rand bleibt die Auswahl stehen und note sagt es; ohne Treffer bleibt die Auswahl, note sagt es
Befehl view-set (nur Seite) meldet Modus, Suche, Filter, hits und hit; schreibt nur, rechnet nichts, wählt nichts — so entsteht keine Schleife
status summary nennt Suche und Position („Suche „space quest“ · Treffer 2 von 7 · Ausgewählt: …“), detail.view den Zustand, die revision schließt view.json ein; hit/hits sind gemerkt, damit das 5-Sekunden-Polling nichts rechnet
Seite beim Öffnen den Zustand übernehmen, in onRefresh übernehmen und die Liste neu holen (Auswahl nicht zurücksetzen), eigene Eingaben entprellt zurückschreiben und nur bei echter Änderung (Vergleich mit dem zuletzt bekannten Zustand), Treffer-Leiste mit Vor/Zurück über denselben Befehl

Regeln: Eine Suche des Agenten sortiert nach Relevanz, solange der Nutzer keine Sortierung vorgibt. Ist die Suche im Chat gemeint, gehört view --query in die SKILL.md — „nur suchen“ zeigt dem Nutzer nichts. Referenz: iris-gamebox (gamebox_cli.py view_command, web/search.html, tests/test_view.py und der End-to-End-Test tests/e2e/).

22.6 Der Baukasten: panel_kit.py und panel_kit.js

Die Technik unter dem Vertrag baut kein Plugin selbst. Kanonische Quelle: _shared/iris-panel-kit/. Das build-irisplugin.sh jedes Plugins mit side_panel kopiert beide Dateien beim Bauen ins Paket (panel_kit.py neben panel_api.py, panel_kit.js neben die HTML-Seite). Keine Kopie im Repo, kein Abgleich nötig, kein Import über Plugin-Grenzen — dasselbe Muster wie _shared/iris-nextcloud-client. Im Repo findet panel_api.py die Quelle über einen sys.path-Fallback (Tests, Entwicklung).

Skript-Seite (panel_api.py):

try:
    import panel_kit as pk          # im Paket neben dieser Datei
except ImportError:                 # im Repo: kanonische Quelle
    sys.path.insert(0, str(Path(__file__).resolve().parents[3] / "_shared" / "iris-panel-kit"))
    import panel_kit as pk

SELECTION = pk.Selection(SUPPORT / "selection.json", "handle")   # "handle": das Pflichtfeld der Auswahl

def cmd_status(body):
    sel = SELECTION.load()
    summary = f"Ausgewählt: {sel['title']}" if sel else "Nichts ausgewählt"
    return pk.status_reply(body, summary, {"selection": sel, "weiteres": "was der Agent per Befehl holt"})

def cmd_select(body):
    handle = str(body.get("handle") or "").strip()
    if handle:
        SELECTION.save(finde_eintrag(handle))      # wirft pk.PanelError, wenn es ihn nicht gibt
    else:
        SELECTION.clear()
    return cmd_status(body)                        # antwortet wie status: der Reiter zeichnet ohne zweite Runde um

pk.run({"status": cmd_status, "select": cmd_select, "selection": lambda b: SELECTION.reply()})
Baustein Aufgabe
pk.run(commands) Dispatch von python3 panel_api.py <befehl>, Body von stdin. PanelError, ValueError und OSError werden zu {ok: false, error} (Exit 0), ein anderer Fehler bleibt ein Traceback — ein Bug soll auffallen
pk.status_reply(body, summary, detail, revision=None, **flach) die Antwort von status: chat_id zurück, summary auf eine Zeile und 240 Zeichen gekürzt, optional revision; **flach trägt zusätzliche Felder auf oberster Ebene, wo eine ältere Seite sie noch liest
pk.ViewState(pfad, defaults, …) der gemeinsame Ansichtszustand, siehe 22.5
pk.revision_of(*pfade, extra=None) die revision aus Dateistatus (Größe, Änderungszeit; Ordner eine Ebene tief) und extra — 12 Hex-Zeichen, siehe 22.4
pk.Selection(pfad, id_feld) die gemeinsame Auswahl: load, save (atomar — das Polling liest mit), clear, reply
pk.safe_chat_id(body) die Chat-Kennung nur, wenn sie wie eine Job-ID aussieht — vor jedem Pfadbau
pk.out(obj), pk.fail(text) Antwort ausgeben und beenden; fail ist {ok: false, error} mit Exit 0

Fehler als Text, nicht als Exit-Zahl. Der Vertrag der panel_api-Route erlaubt beides (Exit ≠ 0 ergibt HTTP 500 mit dem letzten stderr-Satz); das Kit nimmt {ok: false, error}, weil der Client genau dieses Muster kennt (ok: false ergibt „ungültig“ mit error als Grund, Abschnitt 22.3) und der Text so unabhängig von Skript und Weg ankommt.

Seiten-Seite (HTML):

<script src="panel_kit.js"></script>
<script>
  const panel = PanelKit.create({ pluginId: "iris-gamebox" });
  // panel.token, panel.chatId, panel.inTab (true, wenn chat_id in der URL steht: die Seite ist der Reiter)

  async function zeigeDetail(handle) {
    try {
      const d = await panel.api("detail", { handle });   // hängt chat_id an, wirft Error mit lesbarem Text
      …
    } catch (e) { toast(e.message); }
  }
  if (panel.inTab) panel.api("select", { handle });       // nur im Reiter ist die Auswahl gemeinsam
</script>
  • panel.api(cmd, body) hängt chat_id und Token an, wirft bei Transportfehler, HTTP-Fehler, {ok: false} und abweichender chat_id.
  • panel.call(cmd, body) ist dasselbe, gibt aber {ok: false, …} als Ergebnis zurück — für Befehle, deren Ergebnis „fehlgeschlagen, Ausgabe hier“ heißt (iris-solidcraft: mcp-test).
  • panel.onRefresh(fn) meldet einen Handler für iris:panel-refresh an (Abschnitt 22.4); er läuft nie überlappend (ein Nachlauf mit dem jüngsten Anlass), Fehler darin stoppen die übrigen nicht, die Rückgabe meldet ihn wieder ab. Ohne Handler lädt das Kit die Seite neu.
  • panel.poll(fn, ms) ruft fn sofort und dann alle ms, pausiert bei verdecktem Fenster, überlappt nie und gibt eine Stopp-Funktion zurück. Der Reiter pollt status selbst; die Seite braucht poll nur für eigenen Fortschritt (Bau-Jobs).

22.7 Checkliste: ein Plugin bekommt einen Reiter

  1. Manifest: side_panel (Pfad der Seite; kein Objekt, das Reiter-Label ist der name), panel_api mit status in commands (und select/selection, wenn es eine Auswahl gibt), iris_min_version ab der Iris-Version mit Reiter. publish-plugins.sh lehnt side_panel ohne status ab.
  2. panel_api.py mit pk.run(...), status über pk.status_reply — schnell, eine Zeile summary, der Rest in detail.
  3. Die Seite lädt panel_kit.js und ruft nur panel.api/panel.call. Zeigt sie Dinge, die der Agent ändern kann: revision in status (22.4) und panel.onRefresh, das die Daten neu holt, ohne den Zustand der Seite zu verwerfen.
  4. build-irisplugin.sh: panel_kit.py neben panel_api.py, panel_kit.js neben die Seite kopieren; Dateizahl-Prüfung nachziehen.
  5. SKILL.md: dem Agenten sagen, welche Befehle den vollen Stand liefern und was die PLUGIN-PANEL-Zeile bedeutet. Die Zeile selbst erklären beide Server dem Agenten in ihren Basis-Instruktionen — die SKILL.md muss das nicht wiederholen.
  6. Tests: status mit und ohne chat_id, ein Fehlerfall (ok: false mit Text), bei Auswahl select mit unbekanntem Eintrag, revision bleibt gleich bei unverändertem Stand und ändert sich, wenn der Agent etwas ändert.
  7. Prüfen in der echten Iris-App: Reiter öffnen, Auswahl treffen, Nachricht senden — kommt die PLUGIN-PANEL-Zeile an, antwortet der Agent passend? Lässt der Agent etwas ändern: zieht die Ansicht ohne Neuladen nach, und bleibt der Zustand der Seite stehen?

Referenz-Umsetzungen: iris-claude-sessions (ein bestehendes Plugin bekommt einen Reiter: Kern-Funktionen aus den CLI-Befehlen herausgelöst, ViewState für Suche und Papierkorb, Kopf-Cache gegen das Polling, tests/e2e/ mit nachgebauter App), iris-jobs (ein Reiter ohne eigenen Katalog: liest das Job-Register des Reporters, Abbrechen mit Bestätigung, tests/e2e/ gegen echten Reporter), comfyui-workflows (Auswahl über eine Registry, status mit Bibliothek), iris-gamebox (Auswahl aus einem 11-Tausend-Spiele-Katalog, status ohne Vollindex), iris-solidcraft (kein select: die Designs des Chats sind der Stand, geöffnet wird in der App).

23. Suchquellen: wie ein Plugin die Suche des Chats erweitert

„Such in den Chats“ ist der Skill memory. Er durchsucht Iris-Chats, Vorlagen und Workspaces; ein Plugin, das eigene Dokumente hat (Sessions, Notizen, Rezepte), meldet sie als Suchquelle an, und der Skill indexiert sie mit seiner vorhandenen Technik (semantisch und per Volltext), ohne dass der Kern etwas über das Dateiformat des Plugins weiß (iris/iris-plugins#305). Referenz: iris-claude-sessions (Claude-Code-Sessions und Claude-Memory).

Manifest (plugin.json, Feld memory_sources, Liste):

"memory_sources": [
  {"id": "claude-sessions", "label": "Claude-Session", "script": "skills/iris-claude-sessions/claude_sessions.py", "args": ["memory-source", "sessions"]}
]

id (a-z, 0-9, -, höchstens 40 Zeichen, nie eine Kernquelle wie chats) benennt die Quelle im Index, label ist die Herkunft, die jeder Treffer trägt, script ein Pfad innerhalb des Plugins, args die festen Argumente davor. publish-plugins.sh prüft die Form (check_memory_sources).

Der Vertrag (zwei Aufrufe: python3 <script> <args…> list und … text KEY, Antwort ist JSON auf stdout, Fehler auf stderr mit Exit ≠ 0):

Aufruf Antwort
list {"quelle", "label", "dokumente": [{"key", "titel", "gruppe", "datum", "mtime", "aktiv"}]} — nur Metadaten, nie der Text. Billig halten: der Skill ruft es bei jeder Suche.
text KEY {"key", "titel", "text"} für genau ein Dokument. Der Skill ruft es nur für Dokumente, deren mtime er nicht kennt.

Regeln:

  • mtime ändert sich genau dann, wenn sich der Text ändert (Dateizeit oder ein Zähler). Daran erkennt der Skill, was neu zu lesen ist; ein Dokument, das aus list verschwindet, wird aus dem Index entfernt.
  • aktiv: true heißt „wird gerade geschrieben“: nicht neu lesen, den bekannten Stand behalten, nicht löschen (eine laufende Claude-Code-Session ändert sich bei jedem Turn).
  • key ist eine stabile Kennung, die nicht mit - beginnt (argparse läse sie als Option; Claude-Projektordner heißen -Users-…, die Kennung trägt sie ohne das führende -).
  • Nur Lesen. Weder list noch text ändern etwas; ein Test prüft das (Dateizeit und Größe vor und nach).
  • Nur, was suchenswert ist: Gesprächsverlauf statt Werkzeugausgaben (bei Claude-Code-Sessions 5,9 MB statt 799 MB), kein Index oder Inhaltsverzeichnis.
  • Datenschutz: Treffer gehen an das Modell des Chats. Die Anleitung des Plugins sagt das (bei einem Cloud-Modell verlassen sie den Rechner).
  • Nur auf dem eigenen Rechner: Auf einem Linux-Server findet die Suche nur dessen Chats; was ein Plugin nur auf dem Mac kennt, gibt es dort nicht (und die Antwort sagt es).

Stand: Die Plugin-Seite (iris-claude-sessions 0.10.0) und die Prüfung in der Registry stehen; der Skill memory liest memory_sources und indexiert sie in einem nächsten Schritt (#305).