From 39933879774c1525f27b95f78a043b644367c8ee Mon Sep 17 00:00:00 2001 From: Eddy Date: Mon, 20 Apr 2026 03:18:39 +0200 Subject: [PATCH] Security-Fixes + UI-Verbesserungen: Stop-Button, Textfeld, Agent-Filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend: - Credentials aus Code entfernt → ENV-Variablen mit Fallback - File-Traversal in Update-Download verhindert (Path-Sanitization) - CLI-Injection bei D-Bus mit Whitelist-Validierung abgesichert Frontend: - Stop-Button dezenter (kleinere Schrift, gedämpftes Rot, kein Pulsieren) - Stop löscht keine Session/Messages mehr — nur Agents stoppen - Textfeld nicht mehr blockiert während Claude arbeitet (Einwände möglich) - Agent-Filter "Nur aktive" wird in localStorage persistent gespeichert Co-Authored-By: Claude Opus 4.6 --- README.md | 139 +++++++++++++++++++++++++++ src-tauri/src/knowledge.rs | 13 +-- src-tauri/src/programs.rs | 11 +++ src-tauri/src/update.rs | 5 + src/lib/components/AgentView.svelte | 6 +- src/lib/components/ChatPanel.svelte | 6 +- src/lib/components/StopButton.svelte | 48 ++++----- src/lib/stores/events.ts | 8 +- 8 files changed, 192 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 9638a53..7034640 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,145 @@ Eigenständige Desktop-Anwendung die Claude Code als Backend nutzt, mit nativem UI, Live-Übersicht und kontrolliertem OS-Zugriff. +## Status (Stand 2026-04-20) + +**Variante A (Native Desktop-App) ist umgesetzt** — Tauri 2.0 + SvelteKit 5, Phase 1-13 fertig (siehe [ROADMAP.md](ROADMAP.md)). Variante B (autonome VM) bleibt Vision. + +Was läuft: +- 4-Panel-Layout, 24 UI-Komponenten (Chat, Activity, Memory, Audit, Knowledge, Voice, Hooks, IDE, Programs, Performance, Settings, …) +- 16 Rust-Backend-Module (`claude.rs`, `db.rs`, `guard.rs`, `memory.rs`, `voice.rs`, `hooks.rs`, `ide.rs`, …) +- Sprach-Interface mit Push-to-Talk (`VoicePanel`), Whisper STT +- Modell-Auswahl Haiku/Sonnet/Opus, Token-/Kosten-Anzeige +- Subagent-Hierarchie, Multi-Agent-Modi, Hook-System +- Session-Persistenz (SQLite), Audit-Log +- VS-Code-Extension `claude-desktop-bridge` (steuert VSCodium aus der App heraus, WebSocket-Port 7890) +- CI/CD-Pipeline (Forgejo Actions) → AppImage in Package-Registry + +## Installation + +### AppImage (Debian/Ubuntu/Fedora/Arch — **nicht NixOS**) + +```bash +mkdir -p ~/Applications +curl -sSL -o ~/Applications/Claude-Desktop.AppImage \ + -u "token:" \ + 'https://git.data-it-solution.de/api/packages/data/generic/claude-desktop/latest/Claude-Desktop_0.1.0_amd64.AppImage' +chmod +x ~/Applications/Claude-Desktop.AppImage +~/Applications/Claude-Desktop.AppImage +``` + +Auf NixOS hat das AppImage einen WebKit2GTK ↔ Mesa ABI-Konflikt → siehe nächster Abschnitt. + +### NixOS — nativer Build (Pflicht) + +Das AppImage läuft auf NixOS nicht (KB-Eintrag #381 zur Diagnose). Stattdessen lokal bauen mit der mitgelieferten `shell.nix`: + +```bash +cd "/mnt/17 - Entwicklungen/20 - Projekte/ClaudeDesktop" + +# Cargo-Target auf lokales tmpfs (SMB-Mount macht Cargo-Build-Errors) +CARGO_TARGET_DIR=/tmp/claude-target \ + nix-shell shell.nix --run 'npm ci && npm run tauri build -- --bundles appimage' + +# Tauri-Bundling kann an pkg-config-Bug scheitern — Binary reicht aber: +nix-shell shell.nix --run /tmp/claude-target/release/claude-desktop +``` + +Permanenter Wrapper: + +```bash +cat > ~/.local/bin/claude-desktop <<'EOF' +#!/usr/bin/env bash +cd "/mnt/17 - Entwicklungen/20 - Projekte/ClaudeDesktop" +exec nix-shell shell.nix --run /tmp/claude-target/release/claude-desktop +EOF +chmod +x ~/.local/bin/claude-desktop +``` + +### Development + +```bash +cd "/mnt/17 - Entwicklungen/20 - Projekte/ClaudeDesktop" +nix-shell shell.nix --run 'npm ci && npm run tauri:dev' # mit Hot-Reload +``` + +## CI/CD-Pipeline + +Workflow: `.forgejo/workflows/build-appimage.yml` + +| Trigger | Was passiert | +|---|---| +| Push auf `main` mit `[appimage]` in Commit-Message | AppImage bauen + in Package-Registry hochladen | +| Push eines Tags `v*` | zusätzlich als Release-Asset anhängen | + +**Runner:** `16-Forgejo-Runner-AppImage` (Debian Bookworm, glibc, mit linuxdeploy + appimagetool + Tauri-Build-Stack vorinstalliert). Der Standard-Alpine-Runner kann Tauri-AppImages nicht bauen (musl-Inkompatibilität von linuxdeploy). Setup des Runners: KB-Eintrag #371. + +**Workflow-Eigenheiten** (gut zu wissen wenn was ändern): +- AppImage-Filename mit Leerzeichen (`Claude Desktop_…`) wird vor Upload zu `Claude-Desktop_…` umbenannt (curl-URL-Bug) +- Vor jedem Upload werden alte Versionen gelöscht (Forgejo Package-Registry weist PUT auf existierenden Pfad mit HTTP 409 ab) +- Custom-AppRun mit NixOS-Detection wird nach `tauri build` eingesetzt + mit `appimagetool` re-bundled. Der Hook `apprun-hooks/linuxdeploy-plugin-gtk.sh` darf dabei nicht überschrieben werden (KB #384), sonst finden WebKit-Subprozesse ihre Helpers nicht +- Ntfy-Notifications (Build-Start/Success/Failure) — Topic `vk-builds` + +Ntfy-Setup für andere Projekte: KB #190/#191/#220. + +## VS-Code-Extension `claude-desktop-bridge` + +Eigenes Subprojekt unter [vscode-extension/](vscode-extension/). Ermöglicht Claude Desktop, VSCodium fernzusteuern (Datei öffnen, Cursor-Position setzen, Terminal-Befehle absetzen) über einen WebSocket-Server (Port 7890 default). + +```bash +cd vscode-extension +npm ci && npm run compile +# Verpacktes vsix: claude-desktop-bridge-0.1.0.vsix +codium --install-extension claude-desktop-bridge-0.1.0.vsix +``` + +Befehle in VSCodium: `Claude Desktop: Verbindung starten/beenden`. Die App meldet sich beim Start automatisch. + +## Projektstruktur (Stand) + +``` +ClaudeDesktop/ +├── src-tauri/src/ # 16 Rust-Module +│ ├── main.rs # Entry (setzt WEBKIT_DISABLE_*-Defaults für Linux) +│ ├── lib.rs # Tauri-App-Setup +│ ├── claude.rs # Claude Agent SDK Integration +│ ├── db.rs # MySQL (claude-DB) + SQLite-Persistierung +│ ├── guard.rs # Guard-Rails (Critical/Moderate/Safe) +│ ├── hooks.rs # Hook-System +│ ├── memory.rs # Memory/Knowledge-Graph +│ ├── voice.rs # Whisper STT + Voice-Activity-Detection +│ ├── ide.rs # Bridge zur VSCodium-Extension +│ ├── audit.rs, knowledge.rs, programs.rs, session.rs, +│ │ teaching.rs, update.rs, context.rs +│ └── ... +├── src/ # SvelteKit Frontend +│ ├── routes/+layout.svelte +│ ├── routes/+page.svelte +│ ├── routes/presentation/+page.svelte +│ └── lib/components/ # 24 UI-Panels +├── vscode-extension/ # VSCodium-Bridge +├── .forgejo/workflows/ # CI/CD +├── shell.nix # NixOS Dev-Shell +├── ROADMAP.md # Phase-Status +├── TEST-ROADMAP.md # Test-Plan +├── tools.yaml # MCP-Tool-Inventar +└── package.json +``` + +## Wissensbasis-Referenzen (für künftige Sessions) + +| KB-ID | Thema | +|---|---| +| #311 | Diese Pipeline (Workflow-Tricks) | +| #371 | Debian Forgejo-Runner Setup | +| #372 | libssl-dev für openssl-sys | +| #381 | NixOS WebKit2GTK EGL-Crash → native build | +| #382 | Cargo auf SMB → CARGO_TARGET_DIR umleiten | +| #384 | Custom-AppRun + linuxdeploy-Hook | +| #248 | Tauri 2.0 + SvelteKit auf NixOS shell.nix | + +--- + ## Motivation Claude Code in VSCodium funktioniert, hat aber Grenzen: diff --git a/src-tauri/src/knowledge.rs b/src-tauri/src/knowledge.rs index 8f685f7..e9396df 100644 --- a/src-tauri/src/knowledge.rs +++ b/src-tauri/src/knowledge.rs @@ -5,12 +5,13 @@ use mysql_async::{Pool, prelude::*}; use serde::{Deserialize, Serialize}; use chrono::NaiveDateTime; -/// Verbindungskonfiguration -const MYSQL_HOST: &str = "192.168.155.11"; +/// Verbindungskonfiguration — aus ENV-Variablen, Fallback auf Defaults const MYSQL_PORT: u16 = 3306; -const MYSQL_USER: &str = "claude"; -const MYSQL_PASS: &str = "8715"; -const MYSQL_DB: &str = "claude"; + +fn mysql_host() -> String { std::env::var("CLAUDE_MYSQL_HOST").unwrap_or_else(|_| "192.168.155.11".to_string()) } +fn mysql_user() -> String { std::env::var("CLAUDE_MYSQL_USER").unwrap_or_else(|_| "claude".to_string()) } +fn mysql_pass() -> String { std::env::var("CLAUDE_MYSQL_PASS").unwrap_or_else(|_| "8715".to_string()) } +fn mysql_db() -> String { std::env::var("CLAUDE_MYSQL_DB").unwrap_or_else(|_| "claude".to_string()) } /// Wissenseintrag aus der knowledge-Tabelle #[derive(Debug, Clone, Serialize, Deserialize)] @@ -59,7 +60,7 @@ pub struct CategoryInfo { fn create_pool() -> Pool { let url = format!( "mysql://{}:{}@{}:{}/{}", - MYSQL_USER, MYSQL_PASS, MYSQL_HOST, MYSQL_PORT, MYSQL_DB + mysql_user(), mysql_pass(), mysql_host(), MYSQL_PORT, mysql_db() ); Pool::new(url.as_str()) } diff --git a/src-tauri/src/programs.rs b/src-tauri/src/programs.rs index 51c7c6c..e73f940 100644 --- a/src-tauri/src/programs.rs +++ b/src-tauri/src/programs.rs @@ -26,6 +26,17 @@ pub async fn dbus_call( ) -> Result { let bus = if session.unwrap_or(true) { "--session" } else { "--system" }; + // Whitelist-Validierung gegen CLI-Injection + if !service.chars().all(|c| c.is_alphanumeric() || c == '.' || c == '_' || c == '-') { + return Err("Ungültiger Service-Name".to_string()); + } + if !path.chars().all(|c| c.is_alphanumeric() || c == '/' || c == '.' || c == '_' || c == '-') { + return Err("Ungültiger D-Bus Pfad".to_string()); + } + if !method.chars().all(|c| c.is_alphanumeric() || c == '.' || c == '_' || c == '-') { + return Err("Ungültiger Methodenname".to_string()); + } + let mut cmd = Command::new("dbus-send"); cmd.arg("--print-reply") .arg(bus) diff --git a/src-tauri/src/update.rs b/src-tauri/src/update.rs index 24ac052..af8d094 100644 --- a/src-tauri/src/update.rs +++ b/src-tauri/src/update.rs @@ -142,6 +142,11 @@ pub async fn download_update( std::fs::create_dir_all(&cache_dir).ok(); let file_name = download_url.split('/').last().unwrap_or("update.AppImage"); + // Sanitize: Nur Basename, keine Pfad-Traversal (z.B. "../böse.sh") + let file_name = std::path::Path::new(file_name) + .file_name() + .and_then(|f| f.to_str()) + .unwrap_or("update.AppImage"); let download_path = cache_dir.join(file_name); let mut file = std::fs::File::create(&download_path) diff --git a/src/lib/components/AgentView.svelte b/src/lib/components/AgentView.svelte index d928371..e5cf24a 100644 --- a/src/lib/components/AgentView.svelte +++ b/src/lib/components/AgentView.svelte @@ -3,7 +3,11 @@ import type { Agent } from '$lib/stores/app'; import { derived } from 'svelte/store'; - let onlyActive = $state(false); + // Filter-State persistent in localStorage speichern + let onlyActive = $state(localStorage.getItem('agentFilter_onlyActive') === 'true'); + $effect(() => { + localStorage.setItem('agentFilter_onlyActive', String(onlyActive)); + }); // Gefilterter Tree: wenn onlyActive, dann nur Agents mit status==='active' const filteredTree = derived([agentTree], ([$tree]) => { diff --git a/src/lib/components/ChatPanel.svelte b/src/lib/components/ChatPanel.svelte index 0af3968..f7625e8 100644 --- a/src/lib/components/ChatPanel.svelte +++ b/src/lib/components/ChatPanel.svelte @@ -393,7 +393,7 @@ async function sendMessage() { const text = $currentInput.trim(); - if (!text || $isProcessing) return; + if (!text) return; // Auto-Session erstellen falls keine aktiv let sessionId = get(currentSessionId); @@ -747,7 +747,7 @@ bind:value={$currentInput} onkeydown={handleKeydown} placeholder="Nachricht eingeben... (Ctrl+K = Focus, Ctrl+Enter = Senden)" - disabled={$isProcessing || isRecording} + disabled={isRecording} rows="3" >
@@ -768,7 +768,7 @@