- Python 70.6%
- HTML 17.1%
- Shell 7.4%
- Swift 2.3%
- JavaScript 2.2%
- Other 0.4%
| _shared | ||
| blender-asset-generator | ||
| blender-bridge | ||
| blender-comfyui-pipeline | ||
| blender-remesh | ||
| comfyui-bridge | ||
| comfyui-dirigent | ||
| comfyui-workflows | ||
| file-picker | ||
| filmcraft | ||
| freecad-bridge | ||
| heartmula | ||
| ios-install | ||
| iris-browser | ||
| iris-buchhaltung | ||
| iris-claude-sessions | ||
| iris-dev-documentation | ||
| iris-dosgame | ||
| iris-editable-table | ||
| iris-forgejo | ||
| iris-gamebox | ||
| iris-ghidra | ||
| iris-gui | ||
| iris-jobs | ||
| iris-nextcloud-cookbook | ||
| iris-pandoc | ||
| iris-plugin-dev | ||
| iris-rea-recherche | ||
| iris-repo-check | ||
| iris-rustapp-crafter | ||
| iris-server-hosts | ||
| iris-solidcraft | ||
| iris-starter-test | ||
| iris-web-automation | ||
| marker | ||
| marker-project | ||
| mask-editor-reference | ||
| model-convert/scripts | ||
| openmontage | ||
| pdfcraft | ||
| plugin-marktplatz | ||
| qwen-tts-linux | ||
| qwen-tts-mac | ||
| registry-tools | ||
| strata-modelle | ||
| teach | ||
| translate | ||
| translation-matrix | ||
| whisper | ||
| wordcraft | ||
| .gitignore | ||
| build-all-plugins.sh | ||
| CLAUDE.md | ||
| PLUGIN-REGISTRY-REPO.md | ||
| publish-plugins.sh | ||
| README.md | ||
| session-kontext.md | ||
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
.frameworkmehr nötig), der seit 2026-09-12 getrennte Konfigurieren-/ Anwenden-Direktzugriff (Plugins-Chevron) und die seit 2026-09-13 pluralenscheduled_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, Ordnerstore/mitcustom.md), steht in Kapitel 20.Stand 2026-10-16: Plugins entwickeln. Das Plugin
iris-plugin-devfü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 aufiris-plugin-dev/skills/iris-plugin-dev/pd_kern.py(DOKU_THEMEN) ändern.
Inhaltsverzeichnis
- Konzept
- Paketformat:
.irisplugin - plugin.json — Manifest-Referenz
- Lifecycle Hooks
- Verzeichnisstruktur im Paket
- Installierter Zustand (Application Support)
- Entwicklungsworkflow
- build-irisplugin.sh
- Installations-Ablauf in Iris
- Skill-Verknüpfung mit Hermes und Claude Code
- System-Prompt und Templates
- Optionale Komponenten
- Versionierung und Updates
- Konventionen und Einschränkungen
- Settings-Panel & Direktzugriff (Web-Views)
- Referenz-Plugin: Web-Automation
- Scheduled-Job-Vorlagen (System-Vorlagen)
- Veröffentlichung im offiziellen Repository (Plugin-Registry) — früherer Entwurf, ersetzt durch Kapitel 19
- Plugins veröffentlichen:
publish-plugins.shund das Registry-Repo - Marktplatz und Store: Plugins store-tauglich machen
- Rechner, Rechnerwahl und Konfigurations-Zustand (
host_capabilities,local_mode,config-status) - 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/— Versionierungsdatennode_modules/— werden vompost_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.shlehnt ein Paket ab, dasstore/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; beipre_installundpost_installwird die Installation abgebrochen und der Fehler in der Plugin-Karte angezeigt; beipre_uninstallwird 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
Symlinks (von Iris nach Installation angelegt)
~/.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" }
}
Lokale Entwicklung mit Symlinks
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:
- Manifest kopieren
skills/kopierencore/-Verzeichnisse viarsync(exkludiertnode_modules/,.venv/,__pycache__/, etc.)hooks/-Skripte kopieren +chmod +xpython/requirements*.txtkopierensystem_prompt.mdkopierentemplates/*.mdkopierenzip -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"
Schritt 6 — Symlinks anlegen
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_viewladen 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, keinexecute_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-urlersetzt den Standard-PyPI-Index vollständig. Pakete die nicht im alternativen Index liegen (z.B.transformersim PyTorch-Index) werden dann nicht gefunden. Immer--extra-index-urlverwenden.
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
- Nutzer importiert neue
.irisplugin-Version - Iris vergleicht Versions-Strings
- Bei gleicher oder älterer Version: Warnung + Bestätigung nötig
- Altes Verzeichnis →
<id>-<alte-version>.bak - Neue Version durchläuft die vollständige Install-Pipeline inkl. Hooks
- Venv:
pip install -r requirements.txterneut, kein Neu-Anlegen (spart Zeit)
14. Konventionen und Einschränkungen
IDs und Namen
idnur Kleinbuchstaben, Ziffern und Bindestricheidändert sich nie über Versionen hinweg
Hooks
- Skripte müssen
#!/usr/bin/env bashals Shebang haben set -euo pipefailempfohlen- 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/binals erstes PATH-Element; ist dort GNU sed installiert (Homebrewgnu-sed), schlägtsed -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
.envoder API-Keys ins Paket einbetten - API-Keys werden vom Nutzer in der Iris-Keychain-Einstellung hinterlegt
- Hooks können
$HOME/.iris/env.shsourcen wenn nötig (User muss diese Datei selbst anlegen)
Paketgröße
- Ziel: < 10 MB (ohne ML-Gewichte, ohne node_modules)
node_moduleskommt nie ins Paket — post_install-Hook führtnpm installaus- 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 nochnative_framework_pathausschließ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_pathgesetzt → 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, keinusage_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-automationverwaltet 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.htmlals 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 echtenusage_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 geplantenpublish-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.includeim Plugin-Verzeichnis werden veröffentlicht;--allnimmt genau diese, ein ausdrücklich genanntes Plugin ohne.includewird abgelehnt. Das Manifest liest das Skript aus dem gebauten Paket (nicht jedes Plugin hat eineplugin.jsonim Quellbaum). Das Build-Skript eines freigegebenen Plugins mussOUTPUT_DIRaus 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.pydie Texte, die Besucher lesen:name,description,changelogundsystem_prompt_usagederplugin.json, diebeschreibungder Plugin-Actions (iris/Iris#149 — they stand in the consent and install dialog),store/description.md,custom.md,changelog.md,store-page.jsonund 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 dieruntime.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
actionsin its Manifest gets checked before publication:registry-tools/plugin_actions.py(single source of truth, same rules as the app'sPluginActionsCore.swift) validatesactions,autonomous_block_types,launchers,runtimeandplatforms— a defective declaration is refused (registry_index.py add). The Lock-File ofruntime.lockmust 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
versioninplugin.json(und inbuild-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_TOKENals 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 mitnative_framework_pathist es Mac-only.
Einmalig einrichten
- Das Repo
iris/iris-plugin-registryist angelegt (öffentlich, ohne PR-Zwang aufmain, weil das Skript direkt pusht). - Lokaler Klon:
~/Documents/2ndbrain/plugin-registry(ist vorhanden). ./publish-plugins.sh --all --dry-run, danach./publish-plugins.sh --all— erst nach der Prüfung inPLUGIN-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
descriptionaus derplugin.json, Stand (installiert, Update); bei Plugins mit Anleitung oben rechts das Symbol „Anleitung“ (ein Buch). Der erste Satz derdescriptionmuss 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 diedescription), 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 aususer_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 derplugin.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.mdund 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 (changelogaus derplugin.jsonder 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
requiresist 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 meldetregistry_index.py checkeinen 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"].platformslegt fest, wo ein Plugin läuft. Fehlt die Angabe, gilt es als überall lauffähig (außer mitnative_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. Erlaubt: png, jpg, webp, gif, mp4, webm; Namen ausa-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 Ordnerscreenshots/(Bilderstreifen, Reihenfolge nach Dateiname01-,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),
mp4oderwebm, ohne Ton nötig. hero.htmlist die Store-Seite (oben, normalerweise ausstore-page.jsonerzeugt; 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
plugin.jsonfertig (Kategorie,requires,platforms, bei Konfigurationsfensterconfig-statusinpanel_api.commands, bei Rechner-Bedarfhost_capabilities/local_mode),store/gefüllt, Version erhöht../publish-plugins.sh <plugin-id> --dry-run, dann./publish-plugins.sh <plugin-id>(oder--all). Das Skript baut, prüft, trägt inindex.jsonein, committet und pusht die Registry (Kapitel 19). Veröffentlicht wird von Hand, nie automatisch.- Nur Plugins mit
.includeerscheinen im Marktplatz; alle anderen bleiben privat. - 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
- Manifest:
host_capabilities(undlocal_mode, wenn es lokal läuft),panel_apimitconfig-status. - Panel: „Installieren auf:“ mit Picker (
/hosts/targets), nurhost_idspeichern, keine SSH-Felder, „Bitte wählen …“ ohne Wahl. - Laufzeit-Skripte: Kennung über
GET /hosts/resolveauflösen (nichtssh_targetlesen); lokalen Weg ohne SSH lösen. config-statusschreiben und testen (Gelb ohne Wahl, Grün nur mit Feststellung, Standardbibliothek, kein SSH).- Alle Leser der Konfigurationsdatei des Plugins suchen (andere Skills, Tools) und anpassen.
- Veröffentlichen:
./publish-plugins.sh <id> --dry-runprüftconfig-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:
- Die Seite spricht nie direkt mit dem Chat. Sie redet mit
panel_api— über dieselbe generische Route wie jedes Konfigurationsfenster (Kapitel 15). Nicht mit deririsCall-Brücke der Canvas-Blöcke: Die führt Skripte auf dem Client-Gerät aus und existiert auf iOS nicht. - 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.
- 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:
statusist der einzige Pflichtbefehl. Er antwortet mit{ok, chat_id, summary, detail}.summaryist eine Zeile (der Client kappt hart auf 300 Zeichen, das Kit bleibt bei 240) und geht in jede Nachricht;detailgeht nie automatisch in eine Nachricht. Den vollen Stand holt der Agent über Befehle seines Skills — die SKILL.md erklärt ihm welche.statusist 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_idkommt als Quittung zurück. Die Seite schickt sie bei jedem Aufruf mit; fehlt sie in der Antwort vonstatusoder weicht sie ab, zeigt der Reiter einen Fehler statt fremder Daten. Sie ist kein Sicherheitsbeweis — dercfg_tokenbegrenzt 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 ruftselect,panel_apischreibtselection.json,statusnennt die Auswahl vorn in dersummary, der Agent liest sie im Marker oder perselection. 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ängtchat_idund Token an, wirft bei Transportfehler, HTTP-Fehler,{ok: false}und abweichenderchat_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üriris:panel-refreshan (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)ruftfnsofort und dann allems, pausiert bei verdecktem Fenster, überlappt nie und gibt eine Stopp-Funktion zurück. Der Reiter polltstatusselbst; die Seite brauchtpollnur für eigenen Fortschritt (Bau-Jobs).
22.7 Checkliste: ein Plugin bekommt einen Reiter
- Manifest:
side_panel(Pfad der Seite; kein Objekt, das Reiter-Label ist dername),panel_apimitstatusincommands(undselect/selection, wenn es eine Auswahl gibt),iris_min_versionab der Iris-Version mit Reiter.publish-plugins.shlehntside_panelohnestatusab. panel_api.pymitpk.run(...),statusüberpk.status_reply— schnell, eine Zeilesummary, der Rest indetail.- Die Seite lädt
panel_kit.jsund ruft nurpanel.api/panel.call. Zeigt sie Dinge, die der Agent ändern kann:revisioninstatus(22.4) undpanel.onRefresh, das die Daten neu holt, ohne den Zustand der Seite zu verwerfen. build-irisplugin.sh:panel_kit.pynebenpanel_api.py,panel_kit.jsneben die Seite kopieren; Dateizahl-Prüfung nachziehen.- 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. - Tests:
statusmit und ohnechat_id, ein Fehlerfall (ok: falsemit Text), bei Auswahlselectmit unbekanntem Eintrag,revisionbleibt gleich bei unverändertem Stand und ändert sich, wenn der Agent etwas ändert. - 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 auslistverschwindet, wird aus dem Index entfernt.aktiv: trueheißt „wird gerade geschrieben“: nicht neu lesen, den bekannten Stand behalten, nicht löschen (eine laufende Claude-Code-Session ändert sich bei jedem Turn).keyist 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
listnochtextä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).