From 53b3eb651bc551f5d9ecd2f6b846726ef6431ebc Mon Sep 17 00:00:00 2001 From: m3ta-chiron Date: Fri, 18 Sep 2026 09:12:17 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20explain-permissions=20v1.2.0=20?= =?UTF-8?q?=E2=80=94=20Cross-Platform-Notifications=20(az-fleet-wla)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - notify() mit Plattform-Zweigen: Linux notify-send (unverändert), macOS osascript, Windows PowerShell-Toast via WinRT + PowerShell-AUMID (keine App-Registrierung nötig); Titel/Text über env des Child-Prozesses (injektionssicher), XML-Escape via SecurityElement - fail-soft Spawn in Node UND Bun (try/catch + error-Listener decken beide ENOENT-Semantiken ab) — fehlendes Binary/headless = stiller No-Op - Log-Pfad-Entscheidung: Windows %LOCALAPPDATA%\opencode\ (idiomatisch, roamt nicht, Fallback ~\AppData\Local), sonst XDG-State unverändert; dokumentiert im README - Live verifiziert: Linux permission.asked -> ask/ask.explained/replied im Audit-Log + Toast; Fail-Soft win32/darwin-Zweige in node+bun ohne Crash --- README.md | 2 +- explain-permissions/README.md | 34 +++++++- explain-permissions/explain-permissions.js | 94 +++++++++++++++++++--- 3 files changed, 115 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 95bc520..b35bda0 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Das Repos ist **anonym lesbar** (kein Token für Pulls nötig), Schreiben läuft | Plugin | Zweck | Version | |---|---|---| -| [`explain-permissions/`](explain-permissions/) | Erklärt jeden freigabepflichtigen Tool-Call laienverständlich (Was/Folgen/Risiko), Desktop-Notification + Audit-Log | v1.1.0 | +| [`explain-permissions/`](explain-permissions/) | Erklärt jeden freigabepflichtigen Tool-Call laienverständlich (Was/Folgen/Risiko), Desktop-Notification + Audit-Log | v1.2.0 | ## Struktur & Konventionen diff --git a/explain-permissions/README.md b/explain-permissions/README.md index 84670ed..d0a3b81 100644 --- a/explain-permissions/README.md +++ b/explain-permissions/README.md @@ -54,12 +54,40 @@ Pin auf ein Tag (z. B. `explain-permissions/v1.1.0`) oder `main`; anonymes | `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 | `~/.local/state/opencode/explain-permissions.jsonl` | +| `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 ```bash +# Linux/macOS tail -f ~/.local/state/opencode/explain-permissions.jsonl | jq . +# Windows (PowerShell) +Get-Content "$env:LOCALAPPDATA\opencode\explain-permissions.jsonl" -Wait ``` Relevante Events: @@ -77,6 +105,10 @@ Chat-Session im laufenden Prozess reicht nicht. Der Audit-Log-Eintrag ## Versionierung +- `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 diff --git a/explain-permissions/explain-permissions.js b/explain-permissions/explain-permissions.js index 253ab84..d920e15 100644 --- a/explain-permissions/explain-permissions.js +++ b/explain-permissions/explain-permissions.js @@ -1,5 +1,10 @@ // explain-permissions.js — opencode plugin // +// Kanonische Quelle: git.az-gruppe.com/AZ-Intec-GmbH/opencode-plugins +// (Tag explain-permissions/v1.2.0). Fleet-Bezug: az-fleet vendort diese +// Datei nach config/managed/plugins/ (Pin mit sha256 dort in +// roles/managed_config — Byte-Gleichheit ist Vertragsgrundlage). +// // Zweck: Jeden berechtigungspflichtigen Tool-Call laienverständlich erklären — // WAS passiert, WELCHE FOLGEN, WIE RISKANT (Zielgruppe: nicht-technische Nutzer). // @@ -23,6 +28,16 @@ // Fehler, wenn das Modell einen berechtigungspflichtigen Call OHNE vorherige // Text-Erklärung im selben Message-Turn absetzt. Default: AUS (Dialog-Schleifen). // +// Notifications je Plattform (v1.2.0): +// Linux notify-send (libnotify) +// macOS osascript "display notification" (Boardmittel) +// Windows PowerShell-Toast via WinRT; AppUserModelID = PowerShell-AUMID +// (keine App-Registrierung nötig); Titel/Text über env-Variablen des +// Child-Prozesses (injektionssicher), XML-Escape in PowerShell +// Alle Zweige fail-soft: fehlendes Binary, headless Session oder Spawn-Fehler +// sind stille No-Ops (Node und Bun behandeln ENOENT verschieden — try/catch +// plus error-Listener decken beide Semantiken ab). +// // Wichtig: 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. @@ -34,7 +49,10 @@ // OPENCODE_EXPLAIN_ENFORCE=1 Hartes Erzwingen aktivieren (Default: aus) // OPENCODE_EXPLAIN_DEBUG=1 Permission-Events ins Log (Default: aus) // OPENCODE_EXPLAIN_LOG_PATH=… Anderer Log-Pfad -// (Default: ~/.local/state/opencode/explain-permissions.jsonl) +// (Default Windows: %LOCALAPPDATA%\opencode\explain-permissions.jsonl — +// idiomatisch, roamt nicht; Fallback ~\AppData\Local, wenn LOCALAPPDATA +// nicht gesetzt. Entscheidung dokumentiert im README.) +// (Default sonst: ~/.local/state/opencode/explain-permissions.jsonl) // // Log ansehen: // tail -f ~/.local/state/opencode/explain-permissions.jsonl | jq . @@ -45,15 +63,23 @@ import os from "node:os"; import { spawn } from "node:child_process"; const ENV = process.env; -const PLUGIN_VERSION = "1.1.0"; +const PLUGIN_VERSION = "1.2.0"; const NOTIFY = ENV.OPENCODE_EXPLAIN_NOTIFY !== "0"; const LOG = ENV.OPENCODE_EXPLAIN_LOG !== "0"; const INJECT = ENV.OPENCODE_EXPLAIN_INJECT !== "0"; const ENFORCE = ENV.OPENCODE_EXPLAIN_ENFORCE === "1"; const DEBUG = ENV.OPENCODE_EXPLAIN_DEBUG === "1"; -const LOG_PATH = - ENV.OPENCODE_EXPLAIN_LOG_PATH || - path.join(os.homedir(), ".local", "state", "opencode", "explain-permissions.jsonl"); + +// Windows: %LOCALAPPDATA% (idiomatisch, roamt nicht); sonst XDG-State im Profil. +function defaultLogPath() { + if (process.platform === "win32") { + const base = ENV.LOCALAPPDATA || (os.homedir() + "\\AppData\\Local"); + return base + "\\opencode\\explain-permissions.jsonl"; + } + return path.join(os.homedir(), ".local", "state", "opencode", "explain-permissions.jsonl"); +} + +const LOG_PATH = ENV.OPENCODE_EXPLAIN_LOG_PATH || defaultLogPath(); const SYSTEM_PROMPT_RULE = `## Tool-Call-Erklärungen (verpflichtend, laienverständlich) Vor JEDEM Tool-Call, der eine Freigabe erfordert, schreibe unmittelbar davor eine Erklärung auf Deutsch, die ein Mensch ohne Computer-Kenntnisse versteht. Struktur: @@ -118,20 +144,62 @@ function appendLog(entry) { } } -function notify(title, body) { - if (!NOTIFY) return; +// Detached Spawn, fail-soft in Node UND Bun: ENOENT wirft/emittiert in den +// beiden Runtimes unterschiedlich (Node: asynchrones 'error'-Event, Bun kann +// synchron werfen) — try/catch plus error-Listener decken beide Semantiken ab. +// stdio 'ignore' + unref: der Agent-Prozess wartet nie auf das Notification- +// Binary; fehlendes Binary/headless = stiller No-Op. +function runDetached(cmd, args, env) { try { - const child = spawn( - "notify-send", - ["-a", "opencode", "-i", "utilities-terminal", "-u", "normal", title, body.slice(0, 400)], - { detached: true, stdio: "ignore" }, - ); + const child = spawn(cmd, args, { detached: true, stdio: "ignore", env: { ...ENV, ...env } }); + child.on("error", () => {}); child.unref(); } catch { - // keine Desktop-Session (headless) → egal + // fehlendes Binary oder Spawn-Verweigerung → stiller No-Op } } +// PowerShell-AppUserModelID (Shell-Folder-GUID von Windows PowerShell): Toasts +// erscheinen unter dem Namen "Windows PowerShell", ohne dass eine eigene App +// registriert werden muss (AUMID-Standardtrick, kein Shortcut, keine Registry). +const TOAST_AUMID = "{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\\WindowsPowerShell\\v1.0\\powershell.exe"; + +// Titel/Text erreichen PowerShell über env-Variablen des Child-Prozesses — +// kein String-Interpolieren in den Skript-Body (injektionssicher); XML-Escape +// macht [SecurityElement]::Escape auf der Windows-Seite. +const TOAST_PS = [ + "$ErrorActionPreference = 'SilentlyContinue'", + "[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null", + "$xml = New-Object Windows.Data.Xml.Dom.XmlDocument", + "$t = [System.Security.SecurityElement]::Escape($env:OC_TOAST_TITLE)", + "$b = [System.Security.SecurityElement]::Escape($env:OC_TOAST_BODY)", + "$xml.LoadXml('' + $t + '' + $b + '')", + "$toast = New-Object Windows.UI.Notifications.ToastNotification $xml", + "[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('" + TOAST_AUMID + "').Show($toast)", +].join("\n"); + +function windowsToast(title, body) { + const encoded = Buffer.from(TOAST_PS, "utf16le").toString("base64"); + runDetached( + "powershell.exe", + ["-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden", "-EncodedCommand", encoded], + { OC_TOAST_TITLE: title, OC_TOAST_BODY: body }, + ); +} + +function macosNotify(title, body) { + const esc = (s) => s.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); + runDetached("osascript", ["-e", `display notification "${esc(body)}" with title "${esc(title)}"`]); +} + +function notify(title, body) { + if (!NOTIFY) return; + const b = body.slice(0, 400); + if (process.platform === "win32") windowsToast(title, b); + else if (process.platform === "darwin") macosNotify(title, b); + else runDetached("notify-send", ["-a", "opencode", "-i", "utilities-terminal", "-u", "normal", title, b]); +} + function debugLog(source, payload) { if (!DEBUG) return; let serialized;