- New: auth.py – Role-based access (operator/approver/viewer) - New: llm_router.py – Configurable primary/fallback provider chain - New: audit_log.py + audit_query.py – DSGVO-compliant audit trail - New: sanitize_output() – masks secrets in bot responses - Updated: bot.py – Integrated auth guards, router, audit logging - Updated: ollama_client.py – Router-compatible interface - Updated: CLOWNFISCHSERVER.md – Documentation updated - Updated: README.md – Documentation updated
129 lines
4.8 KiB
Python
129 lines
4.8 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
🐠 Clownfischserver
|
||
Author: Mehlzoerwer-Claude (https://github.com/mehlzoerwer-claude)
|
||
License: GPL-3.0 – Keep it open. Always.
|
||
Repo: https://github.com/mehlzoerwer-claude/clownfischserver
|
||
"""
|
||
|
||
import asyncio
|
||
import logging
|
||
import os
|
||
import tempfile
|
||
from pathlib import Path
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
INSTALL_DIR = os.getenv("INSTALL_DIR", "/opt/clownfischserver")
|
||
AIDER_WORKDIR = os.getenv("AIDER_WORKDIR", "/opt/clownfisch-workspace")
|
||
OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434")
|
||
OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "qwen2.5-coder:7b")
|
||
TIMEOUT = 600 # 10 minutes for aider tasks on CPU
|
||
|
||
|
||
class AiderWrapper:
|
||
def __init__(self):
|
||
self.workdir = Path(AIDER_WORKDIR)
|
||
self.workdir.mkdir(parents=True, exist_ok=True)
|
||
self.venv_python = Path(INSTALL_DIR) / "aider-venv" / "bin" / "python3"
|
||
self.venv_aider = Path(INSTALL_DIR) / "aider-venv" / "bin" / "aider"
|
||
logger.info(f"AiderWrapper init: workdir={self.workdir}")
|
||
|
||
async def run(self, task: str, target_file: str = None) -> str:
|
||
"""
|
||
Run aider in script mode for a coding task.
|
||
Returns the output/result as string.
|
||
"""
|
||
logger.info(f"Aider task: {task[:100]}")
|
||
|
||
# Determine working directory and files
|
||
workdir = self.workdir
|
||
|
||
# Build aider command
|
||
cmd = self._build_aider_command(task, target_file)
|
||
|
||
try:
|
||
process = await asyncio.create_subprocess_exec(
|
||
*cmd,
|
||
stdout=asyncio.subprocess.PIPE,
|
||
stderr=asyncio.subprocess.PIPE,
|
||
cwd=str(workdir),
|
||
env={
|
||
**os.environ,
|
||
"OLLAMA_API_BASE": OLLAMA_BASE_URL,
|
||
"HOME": f"/home/{os.getenv('MGMT_USER', 'clownfish')}",
|
||
}
|
||
)
|
||
|
||
try:
|
||
stdout, stderr = await asyncio.wait_for(
|
||
process.communicate(),
|
||
timeout=TIMEOUT
|
||
)
|
||
except asyncio.TimeoutError:
|
||
process.kill()
|
||
return f"⏱️ Aider Timeout nach {TIMEOUT}s. Task zu komplex oder Modell überlastet."
|
||
|
||
stdout_str = stdout.decode("utf-8", errors="replace").strip()
|
||
stderr_str = stderr.decode("utf-8", errors="replace").strip()
|
||
|
||
# Build response
|
||
result_parts = []
|
||
|
||
if stdout_str:
|
||
result_parts.append(stdout_str)
|
||
|
||
if stderr_str and process.returncode != 0:
|
||
result_parts.append(f"[STDERR]\n{stderr_str}")
|
||
|
||
if process.returncode == 0:
|
||
result_parts.insert(0, f"✅ Aider Task abgeschlossen\nArbeitsverzeichnis: {workdir}\n")
|
||
else:
|
||
result_parts.insert(0, f"⚠️ Aider beendet mit Code {process.returncode}\n")
|
||
|
||
return "\n".join(result_parts) or "✓ Task abgeschlossen (keine Ausgabe)"
|
||
|
||
except FileNotFoundError:
|
||
return (
|
||
"❌ Aider nicht gefunden. Bitte prüfen:\n"
|
||
f" ls {self.venv_aider}\n"
|
||
" Ggf. neu installieren mit: pip install aider-chat"
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Aider execution error: {e}", exc_info=True)
|
||
return f"❌ Aider Fehler: {e}"
|
||
|
||
def _build_aider_command(self, task: str, target_file: str = None) -> list:
|
||
"""Build the aider command with appropriate flags"""
|
||
cmd = [
|
||
str(self.venv_aider),
|
||
"--model", f"ollama/{OLLAMA_MODEL}",
|
||
"--no-git", # Don't require git
|
||
"--yes", # Auto-confirm all changes
|
||
"--no-pretty", # Plain output for telegram
|
||
"--no-show-model-warnings", # Skip env var warnings
|
||
"--no-analytics", # No analytics prompts
|
||
"--edit-format", "diff", # Better format for small models
|
||
"--message", task, # The task as a message
|
||
]
|
||
|
||
if target_file:
|
||
cmd.append(target_file)
|
||
|
||
return cmd
|
||
|
||
async def list_workspace_files(self) -> str:
|
||
"""List files in the aider workspace"""
|
||
try:
|
||
files = list(self.workdir.rglob("*"))
|
||
if not files:
|
||
return f"📁 Workspace {self.workdir} ist leer."
|
||
|
||
file_list = "\n".join([
|
||
f" {'📁' if f.is_dir() else '📄'} {f.relative_to(self.workdir)}"
|
||
for f in sorted(files)[:50]
|
||
])
|
||
return f"📁 Workspace: {self.workdir}\n\n{file_list}"
|
||
except Exception as e:
|
||
return f"❌ Fehler beim Lesen des Workspace: {e}"
|