1 Commits
Author SHA1 Message Date
mehlzoerwerandClaude Haiku 4.5 91eeb4834f Initial commit: Clownfischserver v0.5.0
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-30 14:32:33 +02:00
11 changed files with 313 additions and 17 deletions
+15
View File
@@ -0,0 +1,15 @@
{
"permissions": {
"allow": [
"Bash(gh auth *)",
"PowerShell(gh auth *)",
"PowerShell(winget install *)",
"Bash(gh repo *)",
"Bash(git init *)",
"Bash(git remote *)",
"Bash(git add *)",
"Bash(git commit -m 'Initial commit: Clownfischserver v0.5.0 *)",
"Bash(git config *)"
]
}
}
+10 -4
View File
@@ -2,7 +2,13 @@
> Diese Datei ist der zentrale Wissensspeicher für das Clownfischserver-Projekt.
> Lade sie in jede KI/Chat-Session um sofort produktiv zu sein.
> Letzte Aktualisierung: v0.4.13 (18.04.2026)
> Letzte Aktualisierung: v0.5.0 (30.05.2026)
> **⚠️ Bewusst entfernt (nicht wieder einbauen!):** Ein „Vault/MCP-Context"-System
> (`_get_vault_context`, `MCP_VAULT_URL`, `MCP_API_KEY`) wurde in v0.5.0 komplett
> entfernt. Es war toter Code, nutzte `verify=False` und schickte Chat-Nachrichten an
> externe Endpoints – ein klarer DSGVO-Verstoß und Widerspruch zum „local-first"-Prinzip.
> Token-Optimierung über Obsidian gehört NICHT in den Bot.
---
@@ -80,7 +86,7 @@ clownfischserver/
- **Service:** `clownfisch.service` (Deutsch, mit "ch")
- **Linux-User:** Beliebig (z.B. `clownfisch`, `clownfish`, `admin`) – wird aus `MGMT_USER` in `.env` gelesen
- **Branding:** Immer "Clownfisch" (Deutsch)
- **ZIP-Benennung:** `v0.4.13-clownfischserver-opus.zip`
- **ZIP-Benennung:** `v0.5.0-clownfischserver.zip`
### Code-Stil
- Python 3, keine Type Hints in Funktionssignaturen (außer Optional)
@@ -388,7 +394,7 @@ bash -n install.sh
### Schritt 2: ZIP erstellen
```bash
cd .. # Eine Ebene über clownfischserver/
zip -r v0.4.13-clownfischserver-opus.zip clownfischserver/ -x "*__pycache__*" "*.pyc"
zip -r v0.5.0-clownfischserver.zip clownfischserver/ -x "*__pycache__*" "*.pyc"
```
**WICHTIG:**
@@ -400,7 +406,7 @@ zip -r v0.4.13-clownfischserver-opus.zip clownfischserver/ -x "*__pycache__*" "*
### Schritt 3: Verifizieren
```bash
unzip -l v0.4.13-clownfischserver-opus.zip | head -30
unzip -l v0.5.0-clownfischserver.zip | head -30
# Muss zeigen: clownfischserver/bot/bot.py etc. (20 Dateien)
```
+9
View File
@@ -23,6 +23,15 @@ Danke dass du mitmachen willst! Hier sind die Regeln:
- Kommentare auf Deutsch oder Englisch
- Header in jeder Datei (siehe bestehende Dateien)
## Code-Hygiene (wichtig!)
- **Kein toter Code** – keine Funktionen/Module die definiert aber nie aufgerufen werden
- **Keine versteckten Cloud-Calls** – jeder externe Request muss klar ersichtlich und opt-in sein
- **Niemals `verify=False`** bei HTTPS-Requests
- **Keine User-Eingaben an Dritte** ohne explizites Opt-in (DSGVO by design)
- **Klare Conditions** – verschachtelte Ternaries (`A if B else C` innerhalb von `and`-Ketten) vermeiden
- Selbst-Review vor jedem Release: macht jede Zeile noch das was sie soll?
## Issues
- Bug? → Issue mit Log-Output
+11 -4
View File
@@ -6,7 +6,7 @@ Ein selbst-hostbarer, KI-gestützter Server-Manager der ausschließlich über Te
![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)
![Platform: Linux](https://img.shields.io/badge/Platform-Linux-red.svg)
![Version](https://img.shields.io/badge/Version-0.4.13-green.svg)
![Version](https://img.shields.io/badge/Version-0.5.0-green.svg)
![Status](https://img.shields.io/badge/Status-Active%20Development-yellow.svg)
---
@@ -235,7 +235,7 @@ Nach jedem Server-Neustart bekommst du eine Telegram-Nachricht mit Status aller
## Roadmap
### Fertig (v0.1 – v0.4.13)
### Fertig (v0.1 – v0.5.0)
- [x] Basis-Installer mit geführtem Setup (8 Schritte)
- [x] Telegram Bot mit Auth Guard (Single-User)
@@ -265,9 +265,16 @@ Nach jedem Server-Neustart bekommst du eine Telegram-Nachricht mit Status aller
- [x] DSGVO-Regel: `.env` nur per SSH, nie über Telegram lesbar
- [x] Projekt-Kontext-Datei (CLOWNFISCHSERVER.md) für KI-gestütztes Pair-Programming
### v0.5.0 – Geplant
### v0.5.0 – Cleanup & Stabilität (aktuelles Release)
**Sicherheit & DSGVO:**
**Bugfixes & Aufräumarbeiten (dieser Release):**
- [x] Totes Vault/MCP-Context-System komplett entfernt (war nie verdrahtet, `verify=False`-Lücke)
- [x] Privacy-Fix: Chat-Nachrichten gingen via Vault-Suche an externe Endpoints → entfernt
- [x] `/code` Filepath-Logik: fragiler Ternary-Ausdruck sauber umgeschrieben (kein Crash bei leerem Task)
- [x] Aider-Model-Default vereinheitlicht (zieht sich `OLLAMA_MODEL` aus der Umgebung)
- [x] Irreführende „Token-Optimization"-Logs (Anthropic-Kosten/Obsidian) aus dem Start-Log entfernt
**Sicherheit & DSGVO (geplant):**
- [ ] Sensitive Dateien über Telegram blockieren (`.env`, `shadow`, Keys)
- [ ] Output-Sanitizer: API Keys/Tokens in jeder Bot-Antwort maskieren
- [ ] Log-Sanitizer: Keine Secrets im Audit-Log
+1 -1
View File
@@ -17,7 +17,7 @@ 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:2b")
OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "qwen2.5-coder:7b")
TIMEOUT = 600 # 10 minutes for aider tasks on CPU
+20 -4
View File
@@ -98,7 +98,7 @@ async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE):
fallback_info = "\n✅ OpenRouter Fallback aktiv"
await update.message.reply_text(
f"🐠 *Clownfischserver v0.4.13 online.*{fallback_info}\n\n"
f"🐠 *Clownfischserver v0.5.0 online.*{fallback_info}\n\n"
"Schreib mir einfach – ich antworte als Chat.\n\n"
"*Befehle:*\n"
"• `/shell <beschreibung>` – KI generiert Befehl, du bestätigst\n"
@@ -367,7 +367,16 @@ async def cmd_code(update: Update, context: ContextTypes.DEFAULT_TYPE):
workspace = os.getenv("AIDER_WORKDIR", "/opt/clownfisch-workspace")
task_lower = task.lower()
if workspace not in task and "/opt/" not in task and "/" not in task.split()[0] if task.split() else True:
# Only auto-generate a filepath if the task doesn't already point at one.
words = task.split()
first_word_is_path = bool(words) and "/" in words[0]
needs_filepath = (
workspace not in task
and "/opt/" not in task
and not first_word_is_path
)
if needs_filepath:
file_match = re.search(r'(\S+\.(sh|py|js|ts|html|css|yaml|yml|json|conf|cfg|txt))', task)
if file_match:
filename = file_match.group(1)
@@ -679,7 +688,7 @@ async def _handle_update_dialog(update, context, user_input, step):
# =============================================================================
def main():
logger.info("🐠 Clownfischserver v0.4.13 Bot startet...")
logger.info("🐠 Clownfischserver v0.5.0 Bot startet...")
import requests as req
import time
@@ -749,7 +758,14 @@ def main():
app.add_handler(MessageHandler(filters.Document.ALL, handle_file))
logger.info(f"Bot läuft. Authorized Chat-ID: {CHAT_ID}")
app.run_polling(allowed_updates=Update.ALL_TYPES)
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
loop.run_until_complete(app.initialize())
app.run_polling(allowed_updates=Update.ALL_TYPES)
finally:
loop.run_until_complete(app.shutdown())
if __name__ == "__main__":
main()
+1 -1
View File
@@ -152,7 +152,7 @@ class OllamaClient:
except RuntimeError as e:
if "Timeout" not in str(e) and "nicht erreichbar" not in str(e):
raise
logger.warning(f"Ollama nicht verfügbar für /shell – trying OpenRouter fallback")
logger.warning("Ollama nicht verfügbar für /shell – trying OpenRouter fallback")
# 2. Fallback to OpenRouter
if self.openrouter and self.openrouter.is_available():
+3
View File
@@ -103,6 +103,8 @@ class OpenRouterClient:
"""Generate shell command via OpenRouter."""
loop = asyncio.get_running_loop()
system = "Server: Ubuntu 24.04, Clownfischserver setup."
prompt = (
"You are a Linux bash command generator for Ubuntu 24.04.\n"
"Convert the request into a single non-interactive bash command.\n"
@@ -116,6 +118,7 @@ class OpenRouterClient:
None,
lambda: self._chat_sync(
messages=[{"role": "user", "content": prompt}],
system=system,
model=FREE_MODELS["coder"],
),
)
+4
View File
@@ -20,6 +20,10 @@ OLLAMA_NUM_CTX=16384
SNAPSHOT_DIR=/opt/clownfisch-snapshots
SNAPSHOT_METHOD=tar
# HomeIP Auto-Update (optional – myfritz.net Dynamic DNS)
# Domain für automatische Firewall-Regel-Updates bei IP-Wechsel
# CLOWNFISCHSERVER_HOME_IP_DOMAIN=vorname.myfritz.net
# Paths
INSTALL_DIR=/opt/clownfischserver
MGMT_USER=clownfish
+65 -3
View File
@@ -29,8 +29,13 @@ MGMT_USER=""
PKG_MANAGER=""
DISTRO=""
OLLAMA_MODEL=""
OLLAMA_MODEL_FAST=""
SNAPSHOT_METHOD="tar"
OLLAMA_NUM_CTX=4096
MYFRITZ_DOMAIN=""
BOT_TOKEN=""
CHAT_ID=""
OPENROUTER_API_KEY=""
# HELPERS
@@ -105,7 +110,7 @@ print_banner() {
echo -e "${CYAN}${BOLD}"
echo " ╔══════════════════════════════════════════════════════╗"
echo " ║ 🐠 CLOWNFISCHSERVER INSTALLER ║"
echo " ║ v0.4.13 | GPL-3.0 ║"
echo " ║ v0.5.0 | GPL-3.0 ║"
echo " ╚══════════════════════════════════════════════════════╝"
echo -e "${NC}"
echo -e " Willkommen! Dieses Script richtet deinen"
@@ -335,6 +340,34 @@ KNOCKEOF
warn "Bitte Firewall manuell einrichten (iptables/nftables)"
;;
esac
echo ""
echo -e " ${BOLD}${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e " ${BOLD} myfritz – Home-IP Auto-Update (optional)${NC}"
echo -e " ${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e " Wenn du ein AVM-Gerät (Fritzbox) mit myfritz.net hast,"
echo -e " kann der Bot deine Home-IP automatisch ins Firewall-Regelwerk"
echo -e " schreiben. Das bedeutet: SSH bleibt immer erreichbar, auch"
echo -e " wenn sich deine IP ändert (z.B. ISP, mobil, VPN)."
echo ""
echo -e " ${YELLOW}?${NC} ${BOLD}Hast du ein AVM myfritz.net Gerät?${NC} [j/n]"
read -r -p " -> " USE_MYFRITZ
if [[ "$USE_MYFRITZ" =~ ^[jJ]$ ]]; then
echo ""
echo -e " ${YELLOW}?${NC} ${BOLD}Deine myfritz-Domain:${NC}"
echo -e " ${CYAN} Format: vorname.myfritz.net${NC}"
read -r -p " -> " MYFRITZ_DOMAIN
if [[ -z "$MYFRITZ_DOMAIN" ]]; then
MYFRITZ_DOMAIN=""
warn "myfritz übersprungen"
else
ok "myfritz Domain: $MYFRITZ_DOMAIN"
fi
else
MYFRITZ_DOMAIN=""
info "myfritz übersprungen"
fi
}
# HELPER: Pull an Ollama model with progress bar
@@ -681,6 +714,7 @@ SNAPSHOT_METHOD=$SNAPSHOT_METHOD
INSTALL_DIR=$INSTALL_DIR
MGMT_USER=$MGMT_USER
AIDER_WORKDIR=$WORKSPACE_DIR
CLOWNFISCHSERVER_HOME_IP_DOMAIN=$MYFRITZ_DOMAIN
ENVEOF
chmod 600 "$INSTALL_DIR/config/.env"
@@ -696,6 +730,13 @@ step_service() {
cp -r "$SCRIPT_DIR/bot/"* "$INSTALL_DIR/bot/" || fail "Bot-Dateien nicht gefunden – ist der bot/ Ordner dabei?"
ok "Bot-Dateien kopiert"
mkdir -p "$INSTALL_DIR/scripts"
if [ -d "$SCRIPT_DIR/scripts" ]; then
cp -r "$SCRIPT_DIR/scripts/"* "$INSTALL_DIR/scripts/" 2>/dev/null || true
chmod +x "$INSTALL_DIR/scripts/"*.sh 2>/dev/null || true
ok "Scripts kopiert"
fi
chown -R "$MGMT_USER:$MGMT_USER" "$INSTALL_DIR" "$SNAPSHOT_DIR" "$WORKSPACE_DIR"
chmod -R u+w "$INSTALL_DIR/bot"
ok "Verzeichnis-Rechte gesetzt"
@@ -762,6 +803,27 @@ BOOTEOF
run_with_spinner "Boot-Notification aktivieren" bash -c "systemctl daemon-reload && systemctl enable clownfisch-boot"
ok "Boot-Notification: Telegram-Meldung 2 Min nach Serverstart"
# HomeIP Updater – falls myfritz konfiguriert
if [[ -n "$MYFRITZ_DOMAIN" ]]; then
echo ""
info "Installiere HomeIP Auto-Updater (5-Min Cron)..."
# Copy the homeip-updater script if it exists
if [ -f "$SCRIPT_DIR/scripts/clownfischserver-homeip-updater.sh" ]; then
mkdir -p /opt/clownfischserver-scripts
cp "$SCRIPT_DIR/scripts/clownfischserver-homeip-updater.sh" /opt/clownfischserver/scripts/
chmod +x /opt/clownfischserver/scripts/clownfischserver-homeip-updater.sh
# Add cron job (every 5 minutes)
CRON_JOB="*/5 * * * * /opt/clownfischserver/scripts/clownfischserver-homeip-updater.sh >> /var/log/clownfischserver-homeip-updater.log 2>&1"
(crontab -l 2>/dev/null | grep -v "clownfischserver-homeip-updater" ; echo "$CRON_JOB") | crontab - 2>/dev/null || true
ok "HomeIP Auto-Updater installiert (myfritz: $MYFRITZ_DOMAIN)"
else
warn "homeip-updater Script nicht gefunden – übersprungen"
fi
fi
}
# ZUSAMMENFASSUNG
@@ -771,7 +833,7 @@ print_summary() {
echo ""
echo -e "${GREEN}${BOLD}"
echo " +======================================================+"
echo " | CLOWNFISCHSERVER BEREIT! v0.4.13 |"
echo " | CLOWNFISCHSERVER BEREIT! v0.5.0 |"
echo " +======================================================+"
echo -e "${NC}"
echo -e " ${BOLD}Server:${NC} $SERVER_IP"
@@ -862,7 +924,7 @@ main() {
esac
fi
log "=== Clownfischserver v0.4.13 Installation gestartet ==="
log "=== Clownfischserver v0.5.0 Installation gestartet ==="
step_checks
step_packages
step_user
+174
View File
@@ -0,0 +1,174 @@
#!/bin/bash
# =============================================================================
# 🐠 CLOWNFISCHSERVER – HomeIP Auto-Update
# Purpose: Auto-update firewall rule for dynamic home IP (myfritz.net)
# Runs: Every 5 minutes (via cron)
# Author: Mehlzoerwer-Claude
# License: GPL-3.0
# =============================================================================
set -e
# --- Configuration ---
INSTALL_DIR="/opt/clownfischserver"
ENV_FILE="$INSTALL_DIR/config/.env"
STATE_FILE="/var/lib/clownfischserver-homeip-state"
LOG_FILE="/var/log/clownfischserver-homeip-updater.log"
SSH_PORT=22
# --- Helpers ---
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"
}
# --- Load .env ---
load_env() {
if [ ! -f "$ENV_FILE" ]; then
log "ERROR: .env not found at $ENV_FILE"
exit 1
fi
# Load and trim CRLF/LF
MYFRITZ_DOMAIN=$(grep "^CLOWNFISCHSERVER_HOME_IP_DOMAIN=" "$ENV_FILE" | cut -d= -f2 | tr -d '\r\n')
if [ -z "$MYFRITZ_DOMAIN" ]; then
log "ERROR: CLOWNFISCHSERVER_HOME_IP_DOMAIN not set in .env"
exit 1
fi
}
# --- Detect Firewall ---
detect_firewall() {
if systemctl is-active --quiet ufw; then
echo "ufw"
elif systemctl is-active --quiet firewalld; then
echo "firewalld"
elif command -v iptables &>/dev/null; then
echo "iptables"
else
log "ERROR: No firewall detected (ufw/firewalld/iptables)"
exit 1
fi
}
# --- Resolve DNS with fallbacks ---
resolve_ip() {
local domain="$1"
# Try dig first
if command -v dig &>/dev/null; then
IP=$(dig +short "$domain" A | grep -oE '^[0-9.]+$' | head -1)
[ -n "$IP" ] && echo "$IP" && return 0
fi
# Try nslookup
if command -v nslookup &>/dev/null; then
IP=$(nslookup "$domain" 2>/dev/null | grep "Address:" | tail -1 | awk '{print $2}' | grep -oE '^[0-9.]+$')
[ -n "$IP" ] && echo "$IP" && return 0
fi
# Try getent
if command -v getent &>/dev/null; then
IP=$(getent hosts "$domain" | awk '{print $1}' | grep -oE '^[0-9.]+$' | head -1)
[ -n "$IP" ] && echo "$IP" && return 0
fi
log "ERROR: Could not resolve $domain"
return 1
}
# --- Load previous state ---
load_state() {
if [ -f "$STATE_FILE" ]; then
PREVIOUS_IP=$(cat "$STATE_FILE" | grep "^IP=" | cut -d= -f2)
echo "$PREVIOUS_IP"
else
echo ""
fi
}
# --- Save state ---
save_state() {
local ip="$1"
echo "IP=$ip" > "$STATE_FILE"
chmod 600 "$STATE_FILE"
}
# --- UFW Functions ---
ufw_add_rule() {
local ip="$1"
ufw allow from "$ip" to any port "$SSH_PORT" comment "clownfisch-homeip" >/dev/null 2>&1
log "UFW: Added rule for $ip:$SSH_PORT"
}
ufw_remove_rule() {
local ip="$1"
echo "y" | ufw delete allow from "$ip" to any port "$SSH_PORT" >/dev/null 2>&1
log "UFW: Deleted rule for $ip"
}
# --- Firewalld Functions ---
firewalld_add_rule() {
local ip="$1"
firewall-cmd --permanent --add-rich-rule="rule family='ipv4' source address='$ip' port protocol='tcp' port='$SSH_PORT' accept" >/dev/null 2>&1
firewall-cmd --reload >/dev/null 2>&1
log "firewalld: Added rule for $ip:$SSH_PORT"
}
firewalld_remove_rule() {
local ip="$1"
firewall-cmd --permanent --remove-rich-rule="rule family='ipv4' source address='$ip' port protocol='tcp' port='$SSH_PORT' accept" >/dev/null 2>&1
firewall-cmd --reload >/dev/null 2>&1
log "firewalld: Deleted rule for $ip"
}
# --- iptables Functions ---
iptables_add_rule() {
local ip="$1"
iptables -I INPUT -s "$ip" -p tcp --dport "$SSH_PORT" -j ACCEPT
log "iptables: Added rule for $ip:$SSH_PORT"
}
iptables_remove_rule() {
local ip="$1"
iptables -D INPUT -s "$ip" -p tcp --dport "$SSH_PORT" -j ACCEPT 2>/dev/null || true
log "iptables: Deleted rule for $ip"
}
# --- Main ---
main() {
log "=== HomeIP Auto-Update started ==="
load_env
FIREWALL=$(detect_firewall)
NEW_IP=$(resolve_ip "$MYFRITZ_DOMAIN") || exit 1
PREVIOUS_IP=$(load_state)
if [ "$NEW_IP" = "$PREVIOUS_IP" ]; then
log "No change: $NEW_IP (same as previous)"
exit 0
fi
log "IP change detected: $PREVIOUS_IP → $NEW_IP (firewall: $FIREWALL)"
# Remove old rule
if [ -n "$PREVIOUS_IP" ]; then
case "$FIREWALL" in
ufw) ufw_remove_rule "$PREVIOUS_IP" ;;
firewalld) firewalld_remove_rule "$PREVIOUS_IP" ;;
iptables) iptables_remove_rule "$PREVIOUS_IP" ;;
esac
fi
# Add new rule
case "$FIREWALL" in
ufw) ufw_add_rule "$NEW_IP" ;;
firewalld) firewalld_add_rule "$NEW_IP" ;;
iptables) iptables_add_rule "$NEW_IP" ;;
esac
# Save state
save_state "$NEW_IP"
log "=== HomeIP Auto-Update completed successfully ==="
}
main "$@"