Phase 3: SQLite-Persistierung, Guard-Rails Integration + Claude Bridge

- db.rs: Vollständige SQLite-Schicht (Permissions, Audit, Memory, Patterns, Settings)
- guard.rs: Risiko-Klassifikation + Freigabe-Management in lib.rs integriert
- scripts/claude-bridge.js: Node.js Bridge für Claude CLI (stream-json, NDJSON)
- audit.rs + memory.rs: An SQLite angebunden statt In-Memory
- Frontend: MemoryPanel + AuditLog laden echte Daten via Tauri-Commands
- shell.nix: Rust-Toolchain aus nixpkgs statt rustup
- Build: cargo check + npm run build erfolgreich

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Eddy 2026-04-13 18:28:35 +02:00
parent 5003fb9996
commit f5ca5bca7c
10 changed files with 1380 additions and 104 deletions

318
scripts/claude-bridge.js Normal file
View file

@ -0,0 +1,318 @@
#!/usr/bin/env node
// Claude Desktop — Bridge zwischen Tauri-Backend und Claude CLI
//
// Kommunikation: stdin/stdout als NDJSON (eine JSON-Zeile pro Nachricht)
//
// Eingehend (von Tauri):
// { "command": "message", "id": "req-1", "message": "Fixe den Bug..." }
// { "command": "stop", "id": "req-2" }
//
// Ausgehend (an Tauri):
// { "type": "event", "event": "ready" }
// { "type": "event", "event": "agent-started", "payload": { "id": "...", "type": "Main" } }
// { "type": "event", "event": "text", "payload": { "text": "..." } }
// { "type": "event", "event": "tool-start", "payload": { "id": "...", "tool": "Read", "input": {...} } }
// { "type": "event", "event": "tool-end", "payload": { "id": "...", "success": true } }
// { "type": "event", "event": "result", "payload": { "cost": 0.01, "tokens": {...} } }
// { "type": "event", "event": "agent-stopped", "payload": { "id": "..." } }
import { spawn } from 'node:child_process';
import { createInterface } from 'node:readline';
import { randomUUID } from 'node:crypto';
// Aktive Claude-Prozesse
const activeProcesses = new Map();
// Session-ID für --resume
let currentSessionId = null;
// ============ Kommunikation mit Tauri ============
function sendToTauri(msg) {
process.stdout.write(JSON.stringify(msg) + '\n');
}
function sendEvent(event, payload = {}) {
sendToTauri({ type: 'event', event, payload });
}
function sendResponse(id, result) {
sendToTauri({ type: 'response', id, result });
}
function sendError(id, error) {
sendToTauri({ type: 'response', id, error });
}
// ============ Claude CLI aufrufen ============
function spawnClaude(message, requestId) {
const agentId = randomUUID();
const args = [
'-p', // Print-Modus (nicht interaktiv)
'--output-format', 'stream-json', // Streaming JSON Events
'--allowedTools', 'Read', 'Glob', 'Grep', 'Bash', 'Edit', 'Write',
'WebFetch', 'WebSearch', 'Agent',
];
// Bei Fortsetzung letzte Session verwenden
if (currentSessionId) {
args.push('--resume', currentSessionId);
}
// Nachricht als Argument
args.push(message);
// Claude CLI Pfad — npx oder global installiert
const claudePath = process.env.CLAUDE_PATH || 'claude';
const proc = spawn(claudePath, args, {
stdio: ['pipe', 'pipe', 'pipe'],
env: {
...process.env,
// Keine interaktive UI
CLAUDE_CODE_HEADLESS: '1',
},
});
activeProcesses.set(agentId, { proc, requestId });
sendEvent('agent-started', {
id: agentId,
type: 'Main',
task: message.substring(0, 100),
});
// NDJSON-Zeilen von stdout parsen
let fullText = '';
let toolCounter = 0;
const rl = createInterface({ input: proc.stdout });
rl.on('line', (line) => {
if (!line.trim()) return;
try {
const event = JSON.parse(line);
handleClaudeEvent(event, agentId, requestId, { fullText: () => fullText, addText: (t) => { fullText += t; } });
} catch {
// Nicht-JSON Zeile — als Text weiterleiten
fullText += line;
sendEvent('text', { text: line });
}
});
// stderr für Debug-Infos
const stderrRl = createInterface({ input: proc.stderr });
stderrRl.on('line', (line) => {
if (process.env.CLAUDE_DEBUG) {
process.stderr.write(`[claude-stderr] ${line}\n`);
}
});
proc.on('close', (code) => {
activeProcesses.delete(agentId);
sendEvent('agent-stopped', {
id: agentId,
code,
});
if (activeProcesses.size === 0) {
sendEvent('all-stopped');
}
});
proc.on('error', (err) => {
sendError(requestId, `Claude konnte nicht gestartet werden: ${err.message}`);
activeProcesses.delete(agentId);
});
return agentId;
}
// ============ Claude Stream-JSON Events verarbeiten ============
function handleClaudeEvent(event, agentId, requestId, textState) {
// Das stream-json Format hat verschiedene Event-Typen:
// { "type": "assistant", "message": { "content": [...], ... } }
// { "type": "content_block_start", "content_block": { "type": "text", "text": "..." } }
// { "type": "content_block_delta", "delta": { "type": "text_delta", "text": "..." } }
// { "type": "content_block_start", "content_block": { "type": "tool_use", "name": "Read", ... } }
// { "type": "content_block_delta", "delta": { "type": "input_json_delta", ... } }
// { "type": "result", "result": "...", "cost_usd": 0.01, ... }
// { "type": "system", "subtype": "session_id", "session_id": "..." }
const type = event.type;
switch (type) {
case 'system':
if (event.subtype === 'session_id' && event.session_id) {
currentSessionId = event.session_id;
}
break;
case 'assistant':
// Vollständige Nachricht — Inhalt extrahieren
if (event.message?.content) {
for (const block of event.message.content) {
if (block.type === 'text') {
sendEvent('text', { text: block.text });
}
}
}
break;
case 'content_block_start':
if (event.content_block?.type === 'text') {
// Text-Block startet
if (event.content_block.text) {
sendEvent('text', { text: event.content_block.text });
}
} else if (event.content_block?.type === 'tool_use') {
// Tool-Aufruf startet
sendEvent('tool-start', {
id: event.content_block.id || randomUUID(),
tool: event.content_block.name,
input: event.content_block.input || {},
});
}
break;
case 'content_block_delta':
if (event.delta?.type === 'text_delta' && event.delta.text) {
sendEvent('text', { text: event.delta.text });
}
break;
case 'content_block_stop':
// Block fertig — wenn es ein Tool war, Tool-Ende senden
break;
case 'tool_result':
case 'tool_use_result':
sendEvent('tool-end', {
id: event.tool_use_id || event.id || '',
success: !event.is_error,
output: typeof event.content === 'string'
? event.content
: JSON.stringify(event.content)?.substring(0, 500),
});
break;
case 'result':
// Endergebnis mit Kosten
sendEvent('result', {
text: event.result || '',
cost: event.cost_usd || event.cost || 0,
tokens: {
input: event.input_tokens || 0,
output: event.output_tokens || 0,
},
session_id: event.session_id || currentSessionId,
duration_ms: event.duration_ms || 0,
});
// Session-ID für Fortsetzung merken
if (event.session_id) {
currentSessionId = event.session_id;
}
break;
default:
// Unbekannte Events durchreichen
if (event.subagent_id || event.agent_id) {
// Sub-Agent Events
sendEvent('subagent-event', {
agentId: event.subagent_id || event.agent_id,
type,
data: event,
});
}
break;
}
}
// ============ Alle Prozesse stoppen ============
function stopAll() {
for (const [agentId, { proc }] of activeProcesses) {
try {
proc.kill('SIGTERM');
// Nach 2 Sekunden hart killen
setTimeout(() => {
try { proc.kill('SIGKILL'); } catch {}
}, 2000);
} catch {}
}
activeProcesses.clear();
sendEvent('all-stopped');
}
// ============ Eingehende Befehle verarbeiten ============
function handleCommand(msg) {
switch (msg.command) {
case 'message':
if (!msg.message) {
sendError(msg.id, 'Keine Nachricht angegeben');
return;
}
const agentId = spawnClaude(msg.message, msg.id);
sendResponse(msg.id, { agentId, status: 'gestartet' });
break;
case 'stop':
stopAll();
sendResponse(msg.id, { status: 'gestoppt' });
break;
case 'status':
const agents = [];
for (const [id, { proc }] of activeProcesses) {
agents.push({
id,
running: !proc.killed,
pid: proc.pid,
});
}
sendResponse(msg.id, { agents, sessionId: currentSessionId });
break;
case 'ping':
sendResponse(msg.id, { pong: true });
break;
default:
sendError(msg.id, `Unbekannter Befehl: ${msg.command}`);
}
}
// ============ Main ============
// stdin zeilenweise lesen
const rl = createInterface({ input: process.stdin });
rl.on('line', (line) => {
if (!line.trim()) return;
try {
const msg = JSON.parse(line);
handleCommand(msg);
} catch (err) {
process.stderr.write(`Ungültige Eingabe: ${err.message}\n`);
}
});
// Sauber beenden
process.on('SIGTERM', () => {
stopAll();
process.exit(0);
});
process.on('SIGINT', () => {
stopAll();
process.exit(0);
});
// Bereit-Signal senden
sendEvent('ready', { version: '0.1.0', pid: process.pid });

View file

@ -2,7 +2,11 @@
pkgs.mkShell { pkgs.mkShell {
buildInputs = with pkgs; [ buildInputs = with pkgs; [
# Rust (wir nutzen rustup, aber brauchen den Linker) # Rust Toolchain aus nixpkgs
rustc
cargo
rustfmt
clippy
gcc gcc
pkg-config pkg-config
openssl openssl
@ -56,11 +60,9 @@ pkgs.mkShell {
pkgs.openssl pkgs.openssl
]}:$LD_LIBRARY_PATH" ]}:$LD_LIBRARY_PATH"
# Rust von rustup laden
source "$HOME/.cargo/env" 2>/dev/null || true
echo "🦀 Claude Desktop Entwicklungsumgebung geladen" echo "🦀 Claude Desktop Entwicklungsumgebung geladen"
echo " Rust: $(rustc --version 2>/dev/null || echo 'nicht gefunden')" echo " Rust: $(rustc --version 2>/dev/null || echo 'nicht gefunden')"
echo " Cargo: $(cargo --version 2>/dev/null || echo 'nicht gefunden')"
echo " Node: $(node --version 2>/dev/null || echo 'nicht gefunden')" echo " Node: $(node --version 2>/dev/null || echo 'nicht gefunden')"
''; '';
} }

View file

@ -13,7 +13,7 @@ crate-type = ["staticlib", "cdylib", "rlib"]
tauri-build = { version = "2", features = [] } tauri-build = { version = "2", features = [] }
[dependencies] [dependencies]
tauri = { version = "2", features = [] } tauri = { version = "2", features = ["tray-icon"] }
tauri-plugin-shell = "2" tauri-plugin-shell = "2"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"

View file

@ -2,7 +2,10 @@
// Protokolliert alle Änderungen an Einstellungen, Guard-Rails, Hooks, Skills, etc. // Protokolliert alle Änderungen an Einstellungen, Guard-Rails, Hooks, Skills, etc.
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tauri::AppHandle; use std::sync::{Arc, Mutex};
use tauri::{AppHandle, Manager};
use crate::db;
/// Kategorie der Änderung /// Kategorie der Änderung
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@ -105,17 +108,17 @@ pub struct AuditStats {
/// Holt die letzten Audit-Einträge /// Holt die letzten Audit-Einträge
#[tauri::command] #[tauri::command]
pub async fn get_audit_log(limit: Option<usize>) -> Result<Vec<AuditEntry>, String> { pub async fn get_audit_log(app: AppHandle, limit: Option<usize>) -> Result<Vec<AuditEntry>, String> {
let limit = limit.unwrap_or(50); let limit = limit.unwrap_or(50);
let state = app.state::<Arc<Mutex<db::Database>>>();
// TODO: Aus SQLite laden let db_lock = state.lock().unwrap();
db_lock.load_audit_log(limit).map_err(|e| e.to_string())
Ok(vec![])
} }
/// Fügt einen Audit-Eintrag hinzu /// Fügt einen Audit-Eintrag hinzu
#[tauri::command] #[tauri::command]
pub async fn add_audit_entry( pub async fn add_audit_entry(
app: AppHandle,
category: String, category: String,
action: String, action: String,
item_id: String, item_id: String,
@ -159,21 +162,20 @@ pub async fn add_audit_entry(
session_id: None, session_id: None,
}; };
// TODO: In SQLite speichern // In SQLite speichern
let state = app.state::<Arc<Mutex<db::Database>>>();
let db_lock = state.lock().unwrap();
db_lock.save_audit_entry(&entry).map_err(|e| e.to_string())?;
println!("📋 Audit-Eintrag hinzugefügt: {:?}", entry); println!("📋 Audit: {:?} {:?} - {}", entry.action, entry.category, entry.item_name);
Ok(()) Ok(())
} }
/// Holt Audit-Statistiken /// Holt Audit-Statistiken
#[tauri::command] #[tauri::command]
pub async fn get_audit_stats() -> Result<AuditStats, String> { pub async fn get_audit_stats(app: AppHandle) -> Result<AuditStats, String> {
// TODO: Echte Implementierung let state = app.state::<Arc<Mutex<db::Database>>>();
let db_lock = state.lock().unwrap();
Ok(AuditStats { db_lock.audit_stats().map_err(|e| e.to_string())
total: 0,
auto_corrected: 0,
today: 0,
})
} }

458
src-tauri/src/db.rs Normal file
View file

@ -0,0 +1,458 @@
// Claude Desktop — SQLite Datenbankschicht
// Persistiert Guard-Rails, Audit-Log, Memory und Einstellungen
use rusqlite::{params, Connection, Result as SqlResult};
use std::path::Path;
use std::sync::{Arc, Mutex};
use tauri::{AppHandle, Manager};
use crate::audit::{AuditAction, AuditCategory, AuditEntry, AuditStats};
use crate::guard::{Permission, PermissionAction, PermissionType};
use crate::memory::{ContextCategory, MemoryEntry, Pattern};
/// Datenbank-Wrapper
pub struct Database {
conn: Connection,
}
/// Datenbank-Statistiken
#[derive(Debug, serde::Serialize)]
pub struct DbStats {
pub permissions: usize,
pub audit_entries: usize,
pub memory_entries: usize,
pub patterns: usize,
pub db_size_kb: u64,
}
impl Database {
/// Öffnet oder erstellt die Datenbank
pub fn open(path: &Path) -> SqlResult<Self> {
let conn = Connection::open(path)?;
// WAL-Modus für bessere Performance
conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;")?;
let db = Self { conn };
db.create_tables()?;
Ok(db)
}
/// Schema erstellen
fn create_tables(&self) -> SqlResult<()> {
self.conn.execute_batch(
"
-- Guard-Rails Permissions
CREATE TABLE IF NOT EXISTS permissions (
id TEXT PRIMARY KEY,
pattern TEXT NOT NULL,
tool TEXT,
path_pattern TEXT,
action TEXT NOT NULL DEFAULT 'allow',
created_at TEXT NOT NULL,
use_count INTEGER DEFAULT 0,
last_used TEXT
);
-- Audit-Log
CREATE TABLE IF NOT EXISTS audit_log (
id TEXT PRIMARY KEY,
timestamp TEXT NOT NULL,
category TEXT NOT NULL,
action TEXT NOT NULL,
item_id TEXT NOT NULL,
item_name TEXT NOT NULL,
old_value TEXT,
new_value TEXT,
reason TEXT,
auto_corrected INTEGER DEFAULT 0,
session_id TEXT
);
CREATE INDEX IF NOT EXISTS idx_audit_timestamp ON audit_log(timestamp DESC);
CREATE INDEX IF NOT EXISTS idx_audit_category ON audit_log(category);
-- Memory-Einträge
CREATE TABLE IF NOT EXISTS memory (
id TEXT PRIMARY KEY,
category TEXT NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
sticky INTEGER DEFAULT 0,
auto_load INTEGER DEFAULT 0,
last_used TEXT,
use_count INTEGER DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_memory_category ON memory(category);
CREATE INDEX IF NOT EXISTS idx_memory_sticky ON memory(sticky) WHERE sticky = 1;
-- Patterns (Vorgehensweisen)
CREATE TABLE IF NOT EXISTS patterns (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
trigger_text TEXT,
old_approach TEXT,
new_approach TEXT,
reason TEXT,
occurrence_count INTEGER DEFAULT 1,
auto_corrected INTEGER DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
-- Einstellungen (Key-Value)
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at TEXT NOT NULL
);
",
)?;
Ok(())
}
// ============ Permissions ============
/// Speichert eine Permission
pub fn save_permission(&self, perm: &Permission) -> SqlResult<()> {
self.conn.execute(
"INSERT OR REPLACE INTO permissions (id, pattern, tool, path_pattern, action, created_at, use_count, last_used)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
params![
perm.id,
perm.pattern,
perm.tool,
perm.path_pattern,
format!("{:?}", perm.action).to_lowercase(),
perm.created_at,
perm.use_count,
perm.last_used,
],
)?;
Ok(())
}
/// Lädt alle permanenten Permissions
pub fn load_permissions(&self) -> SqlResult<Vec<Permission>> {
let mut stmt = self.conn.prepare(
"SELECT id, pattern, tool, path_pattern, action, created_at, use_count, last_used FROM permissions"
)?;
let perms = stmt.query_map([], |row| {
let action_str: String = row.get(4)?;
let action = match action_str.as_str() {
"deny" => PermissionAction::Deny,
_ => PermissionAction::Allow,
};
Ok(Permission {
id: row.get(0)?,
pattern: row.get(1)?,
tool: row.get(2)?,
path_pattern: row.get(3)?,
permission_type: PermissionType::Permanent,
action,
created_at: row.get(5)?,
use_count: row.get(6)?,
last_used: row.get(7)?,
})
})?.collect::<SqlResult<Vec<_>>>()?;
Ok(perms)
}
/// Löscht eine Permission
pub fn delete_permission(&self, id: &str) -> SqlResult<()> {
self.conn.execute("DELETE FROM permissions WHERE id = ?1", params![id])?;
Ok(())
}
// ============ Audit-Log ============
/// Speichert einen Audit-Eintrag
pub fn save_audit_entry(&self, entry: &AuditEntry) -> SqlResult<()> {
self.conn.execute(
"INSERT INTO audit_log (id, timestamp, category, action, item_id, item_name, old_value, new_value, reason, auto_corrected, session_id)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)",
params![
entry.id,
entry.timestamp,
format!("{:?}", entry.category).to_lowercase(),
format!("{:?}", entry.action).to_lowercase(),
entry.item_id,
entry.item_name,
entry.old_value.as_ref().map(|v| v.to_string()),
entry.new_value.as_ref().map(|v| v.to_string()),
entry.reason,
entry.auto_corrected as i32,
entry.session_id,
],
)?;
Ok(())
}
/// Lädt die letzten N Audit-Einträge
pub fn load_audit_log(&self, limit: usize) -> SqlResult<Vec<AuditEntry>> {
let mut stmt = self.conn.prepare(
"SELECT id, timestamp, category, action, item_id, item_name, old_value, new_value, reason, auto_corrected, session_id
FROM audit_log ORDER BY timestamp DESC LIMIT ?1"
)?;
let entries = stmt.query_map(params![limit as i64], |row| {
let cat_str: String = row.get(2)?;
let act_str: String = row.get(3)?;
let old_val: Option<String> = row.get(6)?;
let new_val: Option<String> = row.get(7)?;
let auto_corr: i32 = row.get(9)?;
Ok(AuditEntry {
id: row.get(0)?,
timestamp: row.get(1)?,
category: parse_audit_category(&cat_str),
action: parse_audit_action(&act_str),
item_id: row.get(4)?,
item_name: row.get(5)?,
old_value: old_val.and_then(|s| serde_json::from_str(&s).ok()),
new_value: new_val.and_then(|s| serde_json::from_str(&s).ok()),
reason: row.get(8)?,
auto_corrected: auto_corr != 0,
session_id: row.get(10)?,
})
})?.collect::<SqlResult<Vec<_>>>()?;
Ok(entries)
}
/// Audit-Statistiken
pub fn audit_stats(&self) -> SqlResult<AuditStats> {
let total: usize = self.conn.query_row(
"SELECT COUNT(*) FROM audit_log", [], |row| row.get(0)
)?;
let auto_corrected: usize = self.conn.query_row(
"SELECT COUNT(*) FROM audit_log WHERE auto_corrected = 1", [], |row| row.get(0)
)?;
let today: usize = self.conn.query_row(
"SELECT COUNT(*) FROM audit_log WHERE timestamp LIKE ?1 || '%'",
params![chrono::Local::now().format("%Y-%m-%d").to_string()],
|row| row.get(0),
)?;
Ok(AuditStats { total, auto_corrected, today })
}
// ============ Memory ============
/// Speichert einen Memory-Eintrag
pub fn save_memory_entry(&self, entry: &MemoryEntry) -> SqlResult<()> {
self.conn.execute(
"INSERT OR REPLACE INTO memory (id, category, key, value, sticky, auto_load, last_used, use_count)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
params![
entry.id,
format!("{:?}", entry.category),
entry.key,
entry.value.to_string(),
entry.sticky as i32,
entry.auto_load as i32,
entry.last_used,
entry.use_count,
],
)?;
Ok(())
}
/// Lädt alle Memory-Einträge
pub fn load_memory_entries(&self) -> SqlResult<Vec<MemoryEntry>> {
let mut stmt = self.conn.prepare(
"SELECT id, category, key, value, sticky, auto_load, last_used, use_count FROM memory"
)?;
let entries = stmt.query_map([], |row| {
let cat_str: String = row.get(1)?;
let val_str: String = row.get(3)?;
let sticky: i32 = row.get(4)?;
let auto_load: i32 = row.get(5)?;
Ok(MemoryEntry {
id: row.get(0)?,
category: parse_context_category(&cat_str),
key: row.get(2)?,
value: serde_json::from_str(&val_str).unwrap_or(serde_json::Value::String(val_str)),
sticky: sticky != 0,
auto_load: auto_load != 0,
last_used: row.get(6)?,
use_count: row.get(7)?,
})
})?.collect::<SqlResult<Vec<_>>>()?;
Ok(entries)
}
/// Löscht einen Memory-Eintrag
pub fn delete_memory_entry(&self, id: &str) -> SqlResult<()> {
self.conn.execute("DELETE FROM memory WHERE id = ?1", params![id])?;
Ok(())
}
// ============ Patterns ============
/// Speichert ein Pattern
pub fn save_pattern(&self, pattern: &Pattern) -> SqlResult<()> {
self.conn.execute(
"INSERT OR REPLACE INTO patterns (id, name, description, trigger_text, old_approach, new_approach, reason, occurrence_count, auto_corrected, created_at, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)",
params![
pattern.id,
pattern.name,
pattern.description,
pattern.trigger,
pattern.old_approach,
pattern.new_approach,
pattern.reason,
pattern.occurrence_count,
pattern.auto_corrected as i32,
pattern.created_at,
pattern.updated_at,
],
)?;
Ok(())
}
/// Lädt alle Patterns
pub fn load_patterns(&self) -> SqlResult<Vec<Pattern>> {
let mut stmt = self.conn.prepare(
"SELECT id, name, description, trigger_text, old_approach, new_approach, reason, occurrence_count, auto_corrected, created_at, updated_at FROM patterns"
)?;
let patterns = stmt.query_map([], |row| {
let auto_corr: i32 = row.get(8)?;
Ok(Pattern {
id: row.get(0)?,
name: row.get(1)?,
description: row.get(2)?,
trigger: row.get(3)?,
old_approach: row.get(4)?,
new_approach: row.get(5)?,
reason: row.get(6)?,
occurrence_count: row.get(7)?,
auto_corrected: auto_corr != 0,
created_at: row.get(9)?,
updated_at: row.get(10)?,
})
})?.collect::<SqlResult<Vec<_>>>()?;
Ok(patterns)
}
// ============ Settings ============
/// Speichert eine Einstellung
pub fn set_setting(&self, key: &str, value: &str) -> SqlResult<()> {
self.conn.execute(
"INSERT OR REPLACE INTO settings (key, value, updated_at) VALUES (?1, ?2, ?3)",
params![key, value, chrono::Local::now().to_rfc3339()],
)?;
Ok(())
}
/// Liest eine Einstellung
pub fn get_setting(&self, key: &str) -> SqlResult<Option<String>> {
let result = self.conn.query_row(
"SELECT value FROM settings WHERE key = ?1",
params![key],
|row| row.get(0),
);
match result {
Ok(val) => Ok(Some(val)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(e),
}
}
// ============ Statistiken ============
/// DB-Statistiken
pub fn stats(&self) -> SqlResult<DbStats> {
let permissions: usize = self.conn.query_row(
"SELECT COUNT(*) FROM permissions", [], |row| row.get(0)
)?;
let audit_entries: usize = self.conn.query_row(
"SELECT COUNT(*) FROM audit_log", [], |row| row.get(0)
)?;
let memory_entries: usize = self.conn.query_row(
"SELECT COUNT(*) FROM memory", [], |row| row.get(0)
)?;
let patterns: usize = self.conn.query_row(
"SELECT COUNT(*) FROM patterns", [], |row| row.get(0)
)?;
// DB-Größe ermitteln
let page_count: u64 = self.conn.query_row(
"PRAGMA page_count", [], |row| row.get(0)
)?;
let page_size: u64 = self.conn.query_row(
"PRAGMA page_size", [], |row| row.get(0)
)?;
let db_size_kb = (page_count * page_size) / 1024;
Ok(DbStats { permissions, audit_entries, memory_entries, patterns, db_size_kb })
}
}
// ============ Hilfsfunktionen ============
fn parse_audit_category(s: &str) -> AuditCategory {
match s {
"guardrail" | "guard_rail" => AuditCategory::GuardRail,
"pattern" => AuditCategory::Pattern,
"hook" => AuditCategory::Hook,
"skill" => AuditCategory::Skill,
"setting" => AuditCategory::Setting,
"mcp" => AuditCategory::MCP,
"memory" => AuditCategory::Memory,
_ => AuditCategory::Setting,
}
}
fn parse_audit_action(s: &str) -> AuditAction {
match s {
"create" => AuditAction::Create,
"update" => AuditAction::Update,
"delete" => AuditAction::Delete,
"enable" => AuditAction::Enable,
"disable" => AuditAction::Disable,
_ => AuditAction::Update,
}
}
fn parse_context_category(s: &str) -> ContextCategory {
match s {
"Critical" => ContextCategory::Critical,
"Pattern" => ContextCategory::Pattern,
"Preference" => ContextCategory::Preference,
"GuardRail" => ContextCategory::GuardRail,
"Hook" => ContextCategory::Hook,
"Skill" => ContextCategory::Skill,
_ => ContextCategory::Pattern,
}
}
// ============ Tauri Commands ============
pub type DbState = Arc<Mutex<Database>>;
/// DB initialisieren (falls Frontend es auslösen will)
#[tauri::command]
pub async fn init_database(app: AppHandle) -> Result<DbStats, String> {
let state = app.state::<DbState>();
let db = state.lock().unwrap();
db.stats().map_err(|e| e.to_string())
}
/// DB-Statistiken abrufen
#[tauri::command]
pub async fn get_db_stats(app: AppHandle) -> Result<DbStats, String> {
let state = app.state::<DbState>();
let db = state.lock().unwrap();
db.stats().map_err(|e| e.to_string())
}

470
src-tauri/src/guard.rs Normal file
View file

@ -0,0 +1,470 @@
// Claude Desktop — Guard-Rails System
// Risiko-Klassifikation und Freigabe-Management
use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Manager};
/// Risiko-Level einer Aktion
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RiskLevel {
Safe, // Read, Glob, Grep → auto-approve
Moderate, // Write, Edit, Git commit → Hinweis in Statusbar
Critical, // Prod-Deploy, DB-Schema, Git push → Modal + Bestätigung
Blocked, // rm -rf, force push main → hart blockiert
}
/// Typ der Freigabe
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PermissionType {
Session, // Gilt nur für aktuelle Session
Permanent, // Dauerhaft gespeichert
}
/// Eine Freigabe-Regel
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Permission {
pub id: String,
pub pattern: String, // z.B. "npm install *", "git commit -m *"
pub tool: Option<String>, // z.B. "Bash", "Edit", None = alle
pub path_pattern: Option<String>, // z.B. "/var/www/dolibarr/*"
pub permission_type: PermissionType,
pub action: PermissionAction,
pub created_at: String,
pub use_count: u32,
pub last_used: Option<String>,
}
/// Aktion einer Regel
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PermissionAction {
Allow,
Deny,
}
/// Anfrage zur Freigabe
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PermissionRequest {
pub id: String,
pub tool: String,
pub command: String,
pub args: Option<serde_json::Value>,
pub path: Option<String>,
pub risk_level: RiskLevel,
pub suggested_pattern: Option<String>,
}
/// Antwort auf Freigabe-Anfrage
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PermissionResponse {
pub request_id: String,
pub allowed: bool,
pub save_as: Option<PermissionType>,
pub pattern: Option<String>,
}
/// Guard-Rails Manager
pub struct GuardRails {
permissions: Vec<Permission>,
session_permissions: Vec<Permission>,
blocked_patterns: Vec<String>,
}
impl Default for GuardRails {
fn default() -> Self {
Self::new()
}
}
impl GuardRails {
pub fn new() -> Self {
Self {
permissions: vec![],
session_permissions: vec![],
blocked_patterns: vec![
// Immer blockiert
"rm -rf /".to_string(),
"rm -rf /*".to_string(),
"rm -rf ~".to_string(),
"git push --force origin main".to_string(),
"git push --force origin master".to_string(),
"git push -f origin main".to_string(),
"DROP DATABASE".to_string(),
"DROP TABLE".to_string(),
"TRUNCATE TABLE".to_string(),
"> /dev/sda".to_string(),
"mkfs.".to_string(),
"dd if=/dev/zero".to_string(),
":(){:|:&};:".to_string(), // Fork bomb
],
}
}
/// Klassifiziert das Risiko einer Aktion
pub fn classify_risk(&self, tool: &str, command: &str, path: Option<&str>) -> RiskLevel {
// Erst prüfen ob blockiert
if self.is_blocked(command) {
return RiskLevel::Blocked;
}
// Tool-basierte Klassifikation
match tool {
// Sichere Tools
"Read" | "Glob" | "Grep" | "WebFetch" | "WebSearch" => RiskLevel::Safe,
// Moderate Tools
"Write" | "Edit" | "NotebookEdit" => {
// Pfad-basierte Eskalation
if let Some(p) = path {
if self.is_production_path(p) {
RiskLevel::Critical
} else {
RiskLevel::Moderate
}
} else {
RiskLevel::Moderate
}
}
// Bash braucht Command-Analyse
"Bash" => self.classify_bash_command(command, path),
// Task/Agent - abhängig vom Typ
"Task" => RiskLevel::Moderate,
// Unbekannt = Moderate
_ => RiskLevel::Moderate,
}
}
/// Klassifiziert Bash-Befehle
fn classify_bash_command(&self, command: &str, path: Option<&str>) -> RiskLevel {
let cmd_lower = command.to_lowercase();
// Sichere Befehle
if cmd_lower.starts_with("ls ")
|| cmd_lower.starts_with("pwd")
|| cmd_lower.starts_with("echo ")
|| cmd_lower.starts_with("cat ")
|| cmd_lower.starts_with("head ")
|| cmd_lower.starts_with("tail ")
|| cmd_lower.starts_with("wc ")
|| cmd_lower.starts_with("grep ")
|| cmd_lower.starts_with("find ")
|| cmd_lower.starts_with("which ")
|| cmd_lower.starts_with("whoami")
|| cmd_lower.starts_with("date")
|| cmd_lower.starts_with("uname")
{
return RiskLevel::Safe;
}
// Kritische Befehle
if cmd_lower.contains("--force")
|| cmd_lower.contains(" -f ")
|| cmd_lower.starts_with("sudo ")
|| cmd_lower.starts_with("su ")
|| cmd_lower.contains("systemctl")
|| cmd_lower.contains("service ")
|| cmd_lower.contains("docker ")
|| cmd_lower.contains("kubectl")
|| cmd_lower.starts_with("rm ")
|| cmd_lower.starts_with("mv ")
|| cmd_lower.contains("chmod ")
|| cmd_lower.contains("chown ")
{
// Prod-Pfad macht es noch kritischer
if let Some(p) = path {
if self.is_production_path(p) {
return RiskLevel::Critical;
}
}
// rm ohne -r ist nur Moderate
if cmd_lower.starts_with("rm ") && !cmd_lower.contains("-r") {
return RiskLevel::Moderate;
}
return RiskLevel::Critical;
}
// Moderate Befehle
if cmd_lower.starts_with("npm ")
|| cmd_lower.starts_with("cargo ")
|| cmd_lower.starts_with("git ")
|| cmd_lower.starts_with("mkdir ")
|| cmd_lower.starts_with("touch ")
|| cmd_lower.starts_with("cp ")
{
// git push ist kritisch
if cmd_lower.contains("git push") {
return RiskLevel::Critical;
}
return RiskLevel::Moderate;
}
// Default: Moderate
RiskLevel::Moderate
}
/// Prüft ob ein Befehl blockiert ist
fn is_blocked(&self, command: &str) -> bool {
let cmd_lower = command.to_lowercase();
self.blocked_patterns
.iter()
.any(|p| cmd_lower.contains(&p.to_lowercase()))
}
/// Prüft ob ein Pfad in Produktion liegt
fn is_production_path(&self, path: &str) -> bool {
let prod_patterns = [
"/var/www/prod",
"/var/www/production",
"/opt/prod",
"/home/prod",
"/srv/prod",
];
prod_patterns.iter().any(|p| path.starts_with(p))
}
/// Prüft ob eine Aktion erlaubt ist
pub fn check_permission(&self, tool: &str, command: &str, path: Option<&str>) -> Option<&Permission> {
// Erst Session-Permissions prüfen
for perm in &self.session_permissions {
if self.matches_permission(perm, tool, command, path) {
return Some(perm);
}
}
// Dann permanente Permissions
for perm in &self.permissions {
if self.matches_permission(perm, tool, command, path) {
return Some(perm);
}
}
None
}
/// Prüft ob eine Permission matcht
fn matches_permission(&self, perm: &Permission, tool: &str, command: &str, path: Option<&str>) -> bool {
// Tool prüfen
if let Some(ref perm_tool) = perm.tool {
if perm_tool != tool {
return false;
}
}
// Pfad prüfen
if let (Some(ref perm_path), Some(actual_path)) = (&perm.path_pattern, path) {
if !self.matches_pattern(perm_path, actual_path) {
return false;
}
}
// Command/Pattern prüfen
self.matches_pattern(&perm.pattern, command)
}
/// Einfacher Pattern-Matcher mit * Wildcard
fn matches_pattern(&self, pattern: &str, value: &str) -> bool {
if pattern == "*" {
return true;
}
if pattern.contains('*') {
let parts: Vec<&str> = pattern.split('*').collect();
if parts.len() == 2 {
let (prefix, suffix) = (parts[0], parts[1]);
return value.starts_with(prefix) && value.ends_with(suffix);
}
}
pattern == value
}
/// Fügt eine Permission hinzu
pub fn add_permission(&mut self, permission: Permission) {
match permission.permission_type {
PermissionType::Session => self.session_permissions.push(permission),
PermissionType::Permanent => self.permissions.push(permission),
}
}
/// Entfernt eine Permission
pub fn remove_permission(&mut self, id: &str) {
self.permissions.retain(|p| p.id != id);
self.session_permissions.retain(|p| p.id != id);
}
/// Session-Permissions löschen
pub fn clear_session(&mut self) {
self.session_permissions.clear();
}
/// Alle Permissions abrufen
pub fn get_all_permissions(&self) -> Vec<&Permission> {
self.permissions.iter().chain(self.session_permissions.iter()).collect()
}
/// Schlägt ein Pattern vor
pub fn suggest_pattern(&self, _tool: &str, command: &str) -> String {
// Für npm install: npm install *
if command.starts_with("npm install ") {
return "npm install *".to_string();
}
// Für git commit: git commit -m *
if command.starts_with("git commit ") {
return "git commit *".to_string();
}
// Für cargo: cargo *
if command.starts_with("cargo ") {
let parts: Vec<&str> = command.split_whitespace().collect();
if parts.len() >= 2 {
return format!("cargo {} *", parts[1]);
}
}
// Default: exakter Befehl
command.to_string()
}
}
// ============ Tauri Commands ============
use std::sync::{Arc, Mutex};
/// Globaler Guard-Rails State
pub type GuardState = Arc<Mutex<GuardRails>>;
/// Prüft eine Aktion und gibt Risiko-Level zurück
#[tauri::command]
pub async fn check_action(
app: AppHandle,
tool: String,
command: String,
path: Option<String>,
) -> Result<serde_json::Value, String> {
let state = app.state::<GuardState>();
let guard = state.lock().unwrap();
let risk = guard.classify_risk(&tool, &command, path.as_deref());
// Wenn blockiert, sofort ablehnen
if risk == RiskLevel::Blocked {
return Ok(serde_json::json!({
"allowed": false,
"risk": "blocked",
"reason": "Diese Aktion ist aus Sicherheitsgründen blockiert."
}));
}
// Permission prüfen
if let Some(perm) = guard.check_permission(&tool, &command, path.as_deref()) {
return Ok(serde_json::json!({
"allowed": perm.action == PermissionAction::Allow,
"risk": format!("{:?}", risk).to_lowercase(),
"matched_rule": perm.id
}));
}
// Kein Match - Frontend muss fragen
let suggested = guard.suggest_pattern(&tool, &command);
Ok(serde_json::json!({
"allowed": risk == RiskLevel::Safe,
"risk": format!("{:?}", risk).to_lowercase(),
"needs_confirmation": risk != RiskLevel::Safe,
"suggested_pattern": suggested
}))
}
/// Fügt eine Freigabe hinzu
#[tauri::command]
pub async fn add_permission(
app: AppHandle,
pattern: String,
tool: Option<String>,
path_pattern: Option<String>,
permission_type: String,
action: String,
) -> Result<String, String> {
let state = app.state::<GuardState>();
let mut guard = state.lock().unwrap();
let perm_type = match permission_type.as_str() {
"session" => PermissionType::Session,
"permanent" => PermissionType::Permanent,
_ => return Err("Ungültiger Permission-Typ".to_string()),
};
let perm_action = match action.as_str() {
"allow" => PermissionAction::Allow,
"deny" => PermissionAction::Deny,
_ => return Err("Ungültige Aktion".to_string()),
};
let id = uuid::Uuid::new_v4().to_string();
let permission = Permission {
id: id.clone(),
pattern,
tool,
path_pattern,
permission_type: perm_type,
action: perm_action,
created_at: chrono::Local::now().to_rfc3339(),
use_count: 0,
last_used: None,
};
guard.add_permission(permission.clone());
// Bei Permanent in SQLite speichern
if perm_type == PermissionType::Permanent {
let db_state = app.state::<Arc<Mutex<crate::db::Database>>>();
let db_lock = db_state.lock().unwrap();
db_lock.save_permission(&permission).map_err(|e| e.to_string())?;
}
println!("✅ Permission hinzugefügt: {}", id);
Ok(id)
}
/// Entfernt eine Freigabe
#[tauri::command]
pub async fn remove_permission(app: AppHandle, id: String) -> Result<(), String> {
let state = app.state::<GuardState>();
let mut guard = state.lock().unwrap();
guard.remove_permission(&id);
// Aus SQLite löschen
let db_state = app.state::<Arc<Mutex<crate::db::Database>>>();
let db_lock = db_state.lock().unwrap();
let _ = db_lock.delete_permission(&id);
println!("🗑️ Permission entfernt: {}", id);
Ok(())
}
/// Holt alle Freigaben
#[tauri::command]
pub async fn get_permissions(app: AppHandle) -> Result<Vec<Permission>, String> {
let state = app.state::<GuardState>();
let guard = state.lock().unwrap();
Ok(guard.get_all_permissions().into_iter().cloned().collect())
}
/// Holt blockierte Patterns
#[tauri::command]
pub async fn get_blocked_patterns(app: AppHandle) -> Result<Vec<String>, String> {
let state = app.state::<GuardState>();
let guard = state.lock().unwrap();
Ok(guard.blocked_patterns.clone())
}

View file

@ -6,6 +6,8 @@ use tauri::Manager;
mod audit; mod audit;
mod claude; mod claude;
mod db;
mod guard;
mod memory; mod memory;
/// Initialisiert die App /// Initialisiert die App
@ -14,6 +16,7 @@ pub fn run() {
tauri::Builder::default() tauri::Builder::default()
.plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_shell::init())
.manage(Arc::new(Mutex::new(claude::ClaudeState::default()))) .manage(Arc::new(Mutex::new(claude::ClaudeState::default())))
.manage(guard::GuardState::new(Mutex::new(guard::GuardRails::new())))
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
// Claude SDK // Claude SDK
claude::send_message, claude::send_message,
@ -28,21 +31,51 @@ pub fn run() {
audit::get_audit_log, audit::get_audit_log,
audit::add_audit_entry, audit::add_audit_entry,
audit::get_audit_stats, audit::get_audit_stats,
// Guard-Rails
guard::check_action,
guard::add_permission,
guard::remove_permission,
guard::get_permissions,
guard::get_blocked_patterns,
// Datenbank
db::init_database,
db::get_db_stats,
]) ])
.setup(|app| { .setup(|app| {
let handle = app.handle().clone(); let handle = app.handle().clone();
println!("🤖 Claude Desktop gestartet"); println!("🤖 Claude Desktop gestartet");
// Gedächtnis-System beim Start laden // Datenbank initialisieren
let app_dir = app.path().app_data_dir()
.expect("Konnte App-Datenverzeichnis nicht ermitteln");
std::fs::create_dir_all(&app_dir).ok();
let db_path = app_dir.join("claude-desktop.db");
println!("📦 Datenbank: {:?}", db_path);
let db = db::Database::open(&db_path)
.expect("Datenbank konnte nicht geöffnet werden");
app.manage(Arc::new(Mutex::new(db)));
// Guard-Rails: permanente Permissions aus DB laden
{
let db_state = handle.state::<Arc<Mutex<db::Database>>>();
let guard_state = handle.state::<guard::GuardState>();
let db_lock = db_state.lock().unwrap();
let mut guard_lock = guard_state.lock().unwrap();
if let Ok(perms) = db_lock.load_permissions() {
for p in perms {
guard_lock.add_permission(p);
}
println!("🛡️ {} permanente Permissions geladen", guard_lock.get_all_permissions().len());
}
}
// Gedächtnis-System laden
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
println!("🧠 Initialisiere Gedächtnis-System..."); println!("🧠 Initialisiere Gedächtnis-System...");
// TODO: memory::load_memory aufrufen
}); });
// Bridge optional beim Start starten (oder lazy bei erster Nachricht)
// let _ = claude::start_bridge(&app.handle());
Ok(()) Ok(())
}) })
.run(tauri::generate_context!()) .run(tauri::generate_context!())

View file

@ -3,7 +3,10 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
use tauri::AppHandle; use std::sync::{Arc, Mutex};
use tauri::{AppHandle, Manager};
use crate::db;
/// Kategorien für Sticky Context (werden nie vergessen) /// Kategorien für Sticky Context (werden nie vergessen)
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
@ -121,44 +124,69 @@ pub struct Pattern {
pub async fn load_memory(app: AppHandle) -> Result<MemoryStats, String> { pub async fn load_memory(app: AppHandle) -> Result<MemoryStats, String> {
println!("🧠 Lade Gedächtnis-System..."); println!("🧠 Lade Gedächtnis-System...");
// TODO: Aus lokaler SQLite laden let state = app.state::<Arc<Mutex<db::Database>>>();
// TODO: Mit Remote claude-db synchronisieren let db_lock = state.lock().unwrap();
let entries = db_lock.load_memory_entries().map_err(|e| e.to_string())?;
let mut by_category: HashMap<String, usize> = HashMap::new();
let mut sticky_count = 0;
for entry in &entries {
let cat = format!("{:?}", entry.category);
*by_category.entry(cat).or_insert(0) += 1;
if entry.sticky {
sticky_count += 1;
}
}
println!("🧠 {} Einträge geladen ({} sticky)", entries.len(), sticky_count);
// Placeholder-Statistiken
Ok(MemoryStats { Ok(MemoryStats {
total: 0, total: entries.len(),
sticky: 0, sticky: sticky_count,
by_category: HashMap::new(), by_category,
}) })
} }
/// Holt den Sticky-Kontext für Claude /// Holt den Sticky-Kontext für Claude
#[tauri::command] #[tauri::command]
pub async fn get_sticky_context() -> Result<Vec<MemoryEntry>, String> { pub async fn get_sticky_context(app: AppHandle) -> Result<Vec<MemoryEntry>, String> {
// TODO: Echte Implementierung let state = app.state::<Arc<Mutex<db::Database>>>();
Ok(vec![]) let db_lock = state.lock().unwrap();
let entries = db_lock.load_memory_entries().map_err(|e| e.to_string())?;
Ok(entries.into_iter().filter(|e| e.sticky).collect())
} }
/// Speichert eine neue Vorgehensweise /// Speichert eine neue Vorgehensweise
#[tauri::command] #[tauri::command]
pub async fn save_pattern(pattern: Pattern) -> Result<(), String> { pub async fn save_pattern(app: AppHandle, pattern: Pattern) -> Result<(), String> {
println!("📝 Speichere Vorgehensweise: {}", pattern.name); println!("📝 Speichere Vorgehensweise: {}", pattern.name);
// TODO: In SQLite speichern let state = app.state::<Arc<Mutex<db::Database>>>();
// TODO: Mit claude-db synchronisieren let db_lock = state.lock().unwrap();
db_lock.save_pattern(&pattern).map_err(|e| e.to_string())
Ok(())
} }
/// Erkennt ein Problem und schlägt Korrektur vor /// Erkennt ein Problem und schlägt Korrektur vor
#[tauri::command] #[tauri::command]
pub async fn detect_issue( pub async fn detect_issue(
app: AppHandle,
error_message: String, error_message: String,
context: String, _context: String,
) -> Result<Option<Pattern>, String> { ) -> Result<Option<Pattern>, String> {
println!("🔍 Prüfe auf bekannte Probleme: {}", &error_message[..error_message.len().min(50)]); let preview_len = error_message.len().min(50);
println!("🔍 Prüfe auf bekannte Probleme: {}", &error_message[..preview_len]);
// TODO: Pattern-Matching gegen bekannte Probleme let state = app.state::<Arc<Mutex<db::Database>>>();
let db_lock = state.lock().unwrap();
let patterns = db_lock.load_patterns().map_err(|e| e.to_string())?;
// Einfaches Pattern-Matching: Trigger im Fehlertext suchen
let error_lower = error_message.to_lowercase();
for pattern in patterns {
if error_lower.contains(&pattern.trigger.to_lowercase()) {
return Ok(Some(pattern));
}
}
Ok(None) Ok(None)
} }

View file

@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { invoke } from '@tauri-apps/api/core';
interface AuditEntry { interface AuditEntry {
id: string; id: string;
@ -20,6 +21,7 @@
// Kategorie-Icons // Kategorie-Icons
const categoryIcons: Record<string, string> = { const categoryIcons: Record<string, string> = {
guard_rail: '🛡️', guard_rail: '🛡️',
guardrail: '🛡️',
pattern: '📋', pattern: '📋',
hook: '🪝', hook: '🪝',
skill: '🎯', skill: '🎯',
@ -37,45 +39,17 @@
disable: '❌' disable: '❌'
}; };
onMount(async () => { async function loadEntries() {
// TODO: Echte Daten laden via Tauri try {
// const result = await invoke('get_audit_log', { limit: 50 }); entries = await invoke('get_audit_log', { limit: 50 });
} catch (err) {
// Placeholder-Daten console.error('Fehler beim Laden des Audit-Logs:', err);
entries = [ }
{
id: '1',
timestamp: new Date().toISOString(),
category: 'guard_rail',
action: 'update',
item_name: 'npm install *',
old_value: 'deny',
new_value: 'allow_permanent',
reason: 'Häufig verwendet, sicher',
auto_corrected: false
},
{
id: '2',
timestamp: new Date(Date.now() - 3600000).toISOString(),
category: 'pattern',
action: 'create',
item_name: 'Tauri in nix-shell starten',
old_value: 'cargo tauri dev',
new_value: 'nix-shell --run "cargo tauri dev"',
reason: 'libbz2 fehlte ohne nix-shell',
auto_corrected: true
},
{
id: '3',
timestamp: new Date(Date.now() - 7200000).toISOString(),
category: 'hook',
action: 'enable',
item_name: 'post-git-commit.sh',
reason: 'Changelog-Erinnerung aktiviert',
auto_corrected: false
}
];
loading = false; loading = false;
}
onMount(() => {
loadEntries();
}); });
function formatTime(timestamp: string): string { function formatTime(timestamp: string): string {

View file

@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { invoke } from '@tauri-apps/api/core';
interface MemoryStats { interface MemoryStats {
total: number; total: number;
@ -27,33 +28,23 @@
MCP: '🔌' MCP: '🔌'
}; };
onMount(async () => { async function loadStats() {
// TODO: Echte Daten laden via Tauri try {
// const result = await invoke('load_memory'); stats = await invoke('load_memory');
lastSync = new Date().toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
// Placeholder-Daten } catch (err) {
stats = { console.error('Fehler beim Laden:', err);
total: 95, }
sticky: 23,
by_category: {
Critical: 8,
Pattern: 47,
Preference: 12,
GuardRail: 15,
Hook: 5,
Skill: 8
}
};
lastSync = 'vor 2 Minuten';
loading = false; loading = false;
}
onMount(() => {
loadStats();
}); });
async function syncNow() { async function syncNow() {
loading = true; loading = true;
// TODO: Mit claude-db synchronisieren await loadStats();
await new Promise((r) => setTimeout(r, 1000));
lastSync = 'gerade eben';
loading = false;
} }
</script> </script>