Files
m3ta-chiron a9ea4361cb fix: E2E-Befunde vm-test 2.0.16 eingearbeitet (az-fleet-7x8)
- Message-Teile liegen im Feld content (nicht parts — SDK-Typen sagen
  parts; beide werden gelesen)
- Persistenz-Lag: Tool-Part ist zur Evaluations-Zeit ggf. noch nicht im
  Message-Kontext — Text-Parts der Call-Message gelten dann als Erklärung
  (liegen per Definition vor dem Call)
- execute.before-Event: Tool-Call-ID im Feld id (callID-Fallback bleibt)
  plus messageID/agent — Shape per Diagnose-Plugin verifiziert
- Event-Namen im Stream: permission.asked/replied (präfix-tolerant)
- inspectCallContext mit DEBUG-Instrumentation (nMsgs/msgFound/partTypes)

E2E vm-test: plugin-loaded 2.0.0, Config-Lesung (Managed+Global+Projekt,
V1-Map), System-Regel je Model-Request (Modell erklärt im Zitat-Block-
Format), evaluate allow+ask (bash→shell-Normalisierung), ask + ask.explained
mit echter Modell-Erklärung. Mock-Tests 17+5 grün.
2026-09-25 10:13:41 +02:00
..

explain-permissions — opencode-Plugin für laienverständliche Tool-Call-Erklärungen

Dieses Verzeichnis ist die kanonische Quelle für das opencode-Plugin explain-permissions. Es lebt im Sammel-Repos opencode-plugins und besteht aus dieser Doku plus je einer Plugin-Fassung pro Major-API — es gibt bewusst keine zweiten Solo-Kopien:

Pfad Fassung Status
v1/explain-permissions.js 1.3.0 (V1-Plugin-API, Einzeldatei) eingefroren — aktiver Fleet-Pin (ADR-0010)
v2/index.js 2.0.0 (V2-Plugin-API, Verzeichnis-Package) V2-only-Cut (az-fleet-7x8) — Pin aktiviert mit dem V2-Rollout

opencode V2 (verifiziert 2.0.16, vm-test) lädt keine Einzeldatei-Plugin- Einträge — file:///…/<datei>.js wird mit der Log-Warnung configured plugin path must be a directory verworfen. v2/ ist deshalb ein Verzeichnis-Package mit index.js als Entrypoint (kein package.json erforderlich); ausgerollt wird das Verzeichnis als Ganzes.

Konsumenten:

Konsument Bezug
Lokale Linux-Kiste (m3tam3re) Repo-Checkout + Symlink in ~/.config/opencode/plugins/
Fleet (az-fleet) später per Repo-Pin (anonym lesbar, kein Token für Pulls)

Zweck

Jeden berechtigungspflichtigen Tool-Call laienverständlich erklären — für Menschen ohne Computer-Kenntnisse. Das Plugin:

  1. Injiziert die Freigabe-Regel in den System-Prompt (Was / Welche Folgen / Wie riskant, auf Deutsch, vor jedem freigabepflichtigen Call) — inklusive der konkreten ask-/allow-Muster aus der opencode-Konfiguration. Seit v1.3.0 mit festem Ausgabeformat: Zitat-Block (>) mit fetten Labels, jeder Punkt auf eigener Zeile. TUI und OpenCode Desktop rendern das als abgesetzten Kasten mit farbigem Balken — die Erklärung hebt sich damit klar vom restlichen Chatverlauf ab, und die drei Punkte bleiben sauber untereinander lesbar. Desktop-Notifications bekommen reinen Text (Markdown-Marker werden entfernt).
  2. Schickt eine Desktop-Notification, wenn der Freigabe-Dialog erscheint — mit der deutschen Erklärung des Modells, Fallback: der Roh-Befehl.
  3. Schreibt ein JSONL-Audit-Log: jeden Freigabe-Request (ask), die abgegebene Erklärung (ask.explained) und die Entscheidung (replied).
  4. Erzwingt optional (Default: aus), dass Erklärungen VOR dem Call kommen — sonst wirft der Call einen Fehler.

Installation

git clone https://git.az-gruppe.com/AZ-Intec-GmbH/opencode-plugins.git ~/p/AZ/opencode-plugins
ln -sf ~/p/AZ/opencode-plugins/explain-permissions/v1/explain-permissions.js ~/.config/opencode/plugins/explain-permissions.js

Danach opencode neu starten (siehe unten). Wichtig: nur EINE Datei dieses Namens im Plugin-Ordner — sonst lädt opencode das Plugin doppelt.

opencode V2 (ab v2.0.0 — V2-only)

v2/index.js exportiert Plugin.define({id: "az.explain-permissions", setup}) als Default-Export. Referenziert wird das Verzeichnis v2/ (umgenannt nach Wunsch, z. B. explain-permissions-v2/) unter dem V2-Schlüssel plugins mit absoluter file:///-URL:

{
  "plugins": [{ "package": "file:///C:/ProgramData/opencode/plugins/explain-permissions-v2" }]
}

Alternativ läuft das Verzeichnis auch unverändert als Plugin-Package in ~/.config/opencode/plugins/ bzw. .opencode/plugins/ (dort automatisch entdeckt, kein Config-Eintrag nötig). Benötigt opencode V2 (Core 2.0.x); V1 (1.18.x) lädt es bewusst NICHT (V2-only-Cut) — die V1-Fleet bleibt auf der gepinnten v1.3.0.

Fleet

Pin auf ein Tag (z. B. explain-permissions/v2.0.0) oder main; anonymes git clone über HTTPS reicht, kein Token nötig.

Konfiguration via Environment-Variablen

Variable Wirkung Default
OPENCODE_EXPLAIN_NOTIFY=0 Desktop-Notifications abschalten an
OPENCODE_EXPLAIN_LOG=0 Audit-Log abschalten an
OPENCODE_EXPLAIN_INJECT=0 System-Prompt-Regel abschalten an
OPENCODE_EXPLAIN_ENFORCE=1 Hartes Erzwingen der Erklärung aktivieren (Call ohne vorherige Erklärung → Fehler) aus
OPENCODE_EXPLAIN_DEBUG=1 Permission-Events ins Log schreiben aus
OPENCODE_EXPLAIN_LOG_PATH=… Anderer Log-Pfad plattformabhängig (siehe unten)

Notifications je Plattform (v1.2.0)

Plattform Mechanismus Anmerkung
Linux notify-send (libnotify) wie bisher
macOS osascript display notification Boardmittel, keine Abhängigkeit
Windows PowerShell-Toast (WinRT) via -EncodedCommand AppUserModelID = PowerShell-AUMID — keine App-Registrierung nötig; Titel/Text laufen über env-Variablen des Child-Prozesses (injektionssicher), XML-Escaping in PowerShell

Alle Zweige sind fail-soft: fehlendes Binary, headless Session oder Spawn-Fehler sind stille No-Ops — kein Crash, kein Log-Müll. Node und Bun behandeln ENOENT beim Spawn unterschiedlich (asynchrones error-Event vs. synchroner Wurf); das Plugin deckt beide Semantiken ab (try/catch plus error-Listener) und läuft damit in beiden Runtime-Welten.

Log-Pfad-Entscheidung (v1.2.0)

Plattform Default-Pfad Begründung
Windows %LOCALAPPDATA%\opencode\explain-permissions.jsonl idiomatisch, roamt nicht, liegt neben anderen App-States; Fallback ~\AppData\Local\…, falls LOCALAPPDATA nicht gesetzt
Linux/macOS ~/.local/state/opencode/explain-permissions.jsonl XDG-State-Konvention (unverändert seit v1.1.0)

~/.local/state würde zwar überall funktionieren (auch auf Windows), aber %LOCALAPPDATA% ist auf Windows der etablierte Ort für pro-Nutzer-State — Pilotnutzer-Support findet Dateien dort, wo Windows sie erwartet.

Audit-Log ansehen

# Linux/macOS
tail -f ~/.local/state/opencode/explain-permissions.jsonl | jq .
# Windows (PowerShell)
Get-Content "$env:LOCALAPPDATA\opencode\explain-permissions.jsonl" -Wait

Relevante Events:

  • plugin-loaded — mit version: welche Plugin-Version wann geladen wurde
  • ask — Freigabe-Request (Roh-Call, Muster, Session)
  • ask.explained — die Erklärung, die das Modell vor dem Call abgegeben hat
  • replied — die Entscheidung (once / always / reject)

Wichtig: Neustart

Plugin-Änderungen greifen erst nach Neustart von opencode — eine neue Chat-Session im laufenden Prozess reicht nicht. Der Audit-Log-Eintrag plugin-loaded (mit Version) zeigt an, welche Version wann geladen wurde. Unter V2 gilt zusätzlich: Der Background-Service besitzt und cachet die Konfiguration — Config-/Plugin-Änderungen wirken erst nach opencode service restart; Änderungen an beobachteten Plugin-Verzeichnissen laden automatisch neu.

V2-Migrationsmappe (v2.0.0, az-fleet-7x8)

V1 (1.18.x) V2 (2.0.x)
export const ExplainPermissionsPlugin = async ({client}) => … (Hooks-Objekt) export default Plugin.define({id: "az.explain-permissions", async setup(ctx)}), Hooks via ctx.*.hook() registriert
Config-Schlüssel "plugin": ["file:///…"] "plugins": [{"package": "file:///…"}] (Einzel-Datei-Einträge unterstützt)
hooks.config (liest permission-Sektion) entfallen — Config-Dateien direkt per fs gelesen (Managed-Verzeichnis, Global, Projekt, OPENCODE_CONFIG), beide Formate: V1-permission-Map + V2-permissions-Array; Action-Renames bash→shell, task→subagent, write/patch→edit; lsp/doom_loop sind in V2 tote Actions und werden verworfen
permission.ask-Hook + permission.asked/updated-Events ctx.permission.hook("evaluate") — feuert für allow UND ask nach Regel-Evaluation, vor Dialog/Ausführung (explizites deny ruft den Hook nicht); Effect ask → recordAsk + explainAndNotify
permission.replied-Event permission.replied im öffentlichen Event-Stream (ctx.event.subscribe), Payload {sessionID, requestID, reply} — feuert nur bei echter Client-Antwort, NICHT bei Non-Interactive-Auto-Reject (vm-test 2.0.16)
experimental.chat.system.transform ctx.session.hook("context") → event.system.push({type: "text", text}) — Agent-Loop inkl. Tool-Continuations
tool.execute.before (ENFORCE) ctx.tool.hook("execute.before") — Event {tool, sessionID, callID, input}
client.session.messages({path:{id}}) ctx.session.context({sessionID}) — Message-Teile im Feld content (2.0.16; parts wird tolerierend mitgelesen), Tool-Parts tragen die Call-ID als id (matcht source.id der Permission-Evaluation); im Flight befindliche Assistant-Messages sind bereits sichtbar
plugin-loaded-Audit-Eintrag beim Laden in setup() (jetzt mit Plugin-ID, opencode-Version, Location)

Unverändert übernommen: Notification-Logik je Plattform (PowerShell-WinRT-AUMID, injektionssicher), JSONL-Audit-Log samt Env-Flags, fail-soft-Muster, das Erklär-Format (Zitat-Block) und die ENFORCE-Semantik (greift nur auf Calls, die durch einen Freigabe-Dialog gegangen sind).

Der @opencode/plugin-Import ist bewusst dynamisch mit Fallback auf die Rohestform {id, setup}: Schlägt die Paketauflösung fehl (Einzel-Datei-Package außerhalb eines npm-Kontexts), bleibt der Fleet-Load fail-soft und das Plugin trotzdem aktiv, statt beim Laden der ganzen Datei zu werfen.

Versionierung

  • explain-permissions/v2.0.0 — V2-only-Port auf die V2-Plugin-API (Core 2.0.x, az-fleet-7x8): Plugin.define-Default-Export, Hooks via ctx.permission.hook("evaluate") / ctx.session.hook("context") / ctx.tool.hook("execute.before"), Event-Stream permission.asked / permission.replied, Config-Lesung per fs mit V1+V2-Parser (JSONC-tolerant) statt hooks.config, Action-Namen auf V2 (bash→shell, task→subagent; lsp/doom_loop verworfen). Layout: Verzeichnis-Package v2/index.js — V2 (2.0.16) verwirft Einzeldatei- Einträge („configured plugin path must be a directory", vm-test-Evidenz). KEIN V1-Export mehr (V2-only-Cut, Entscheidung 25.09.) — V1-Fleet bleibt gepinnt auf v1.3.0. Verifikation: Mock-ctx-Tests 21/21 grün (Config-Parsing beider Formen, System-Regel-Injektion, evaluate/ask-Pfad, Hook↔Event-Dedup, replied-Audit, ENFORCE); E2E vm-test (2.0.16): Plugin-Load (plugin-loaded 2.0.0), Config-Lesung Global+Managed+Projekt, System-Regel je Model-Request, evaluate-Hook allow+ask mit V1-Config-Normalisierung (bash→shell), ask-Audit + Notification-Pfad, Einzeldatei-Ablehnung reproduziert.
  • explain-permissions/v1.3.0 — Erklärungs-Format überarbeitet: Zitat-Block (>) mit fetten Labels (**WAS passiert:** usw.), jeder Punkt auf eigener Zeile — bessere Lesbarkeit und visuelle Hervorhebung in TUI und Desktop (beide rendern Blockquotes mit farbigem Balken; verifiziert gegen opencode 1.18.30, das keinen Anzeige-Transform-Hook bietet — experimental.chat.messages.transform wirkt nur modellseitig). Desktop-Notifications erhalten reinen Text ohne Markdown-Marker; EXPLAIN_FIRST_ERROR verweist auf das neue Format.
  • explain-permissions/v1.2.0 — Cross-Platform-Notifications (Linux notify-send / macOS osascript / Windows PowerShell-Toast mit PowerShell-AUMID, keine App-Registrierung), fail-soft Spawn in Node- und Bun-Runtimes, Windows-Log-Pfad %LOCALAPPDATA%\opencode\ (Entscheidung siehe oben).
  • explain-permissions/v1.1.0 — Baseline, exakt der Stand der lokalen Solo-Datei vom 17.09.2026 (Inhaltsgleichheit per SHA-256 verifiziert).
  • Fortlaufende Versionsnummer in PLUGIN_VERSION im Dateikopf; Änderungen bekommen ein Tag explain-permissions/v<version> in diesem Repo.