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:
parent
5003fb9996
commit
f5ca5bca7c
10 changed files with 1380 additions and 104 deletions
318
scripts/claude-bridge.js
Normal file
318
scripts/claude-bridge.js
Normal 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 });
|
||||||
10
shell.nix
10
shell.nix
|
|
@ -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')"
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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
458
src-tauri/src/db.rs
Normal 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
470
src-tauri/src/guard.rs
Normal 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())
|
||||||
|
}
|
||||||
|
|
@ -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!())
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue