feat: explain-permissions v1.2.0 — Cross-Platform-Notifications (az-fleet-wla)
- 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
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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('<toast><visual><binding template=\"ToastGeneric\"><text>' + $t + '</text><text>' + $b + '</text></binding></visual></toast>')",
|
||||
"$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;
|
||||
|
||||
Reference in New Issue
Block a user