# ByteClaw Schlanker, selbst gehosteter KI-Assistent: **Go-Backend** (ein statisches Binary, kein CGO) + **nativer Android-Client** (Kotlin, Jetpack Compose). ## Architektur ``` Android-App (Compose) Go-Backend (cmd/byteclaw) ├─ Einstellungen (EncryptedSharedPrefs) ├─ internal/server HTTP-API + WebSocket, Bearer-Token, Origin-Check ├─ Chat ──── WebSocket /api/ws ───────▶ ├─ internal/agent Agent-Loop mit Tool-Calling, Lock pro Session └─ Sessions ─ REST /api/sessions ──────▶ ├─ internal/llm Provider-Interface + OpenAI-kompatibler Streaming-Provider ├─ internal/tools time_now, web_fetch, file_read/_write/_list ├─ internal/store SQLite (modernc.org/sqlite, pure Go) └─ internal/config YAML/JSON + Umgebungsvariablen ``` Ablauf einer Nachricht: Client → `/api/ws` → Agent speichert User-Nachricht → LLM (Streaming) → bei Tool-Calls werden Tools ausgeführt, Ergebnisse gespeichert und das LLM erneut gefragt (max. `agent.max_iterations`) → Tokens werden live als `delta`-Events gestreamt, am Ende `done`. Abhängigkeiten des Backends: `gopkg.in/yaml.v3`, `modernc.org/sqlite`, `github.com/coder/websocket`. ## Sichere Defaults - Start verweigert ohne `auth_token` (mind. 16 Zeichen); Vergleich in konstanter Zeit. - Lauscht standardmäßig nur auf `127.0.0.1:8787`. - Browser-Origins: nur gleicher Host oder explizit in `allowed_origins`; `*` muss bewusst gesetzt werden. Native Clients (ohne `Origin`-Header) brauchen trotzdem das Token. - TLS-Zertifikatsprüfung ist überall an (LLM-Provider, `web_fetch`); es gibt keinen Schalter zum Abschalten. - `web_fetch`: nur http(s), Timeout, Größenlimit, max. 5 Redirects, blockiert Loopback/Private/Link-Local/CGNAT-Adressen beim Verbindungsaufbau (schützt auch gegen DNS-Rebinding und Redirect-Tricks). Abschaltbar per `allow_private`. - Datei-Tools sind per `os.Root` auf den Workspace beschränkt (kein `..`, keine absoluten Pfade, keine Symlink-Ausbrüche) und größenbegrenzt. - Kein Shell-Tool. - Docker: distroless, User `65532` (nonroot), Go-Version fixiert. - Android: Release-Builds erlauben nur HTTPS/WSS; Klartext (http/ws) nur im Debug-Build für lokale Entwicklung. Token/URL verschlüsselt per Android Keystore. ## Quickstart Backend ```bash cp config.example.yaml config.yaml export BYTECLAW_AUTH_TOKEN=$(openssl rand -hex 32) export BYTECLAW_LLM_API_KEY=sk-... # OpenAI / OpenRouter; für Ollama leer lassen make run # = go build + ./bin/byteclaw -config config.yaml ``` Provider-Presets (`llm.provider` bzw. `BYTECLAW_LLM_PROVIDER`). Ein Preset füllt `base_url` und `model`, wenn sie leer sind; explizite Werte haben Vorrang. Der API-Key fällt auf die genannte Umgebungsvariable zurück. Alle nutzen `Authorization: Bearer ` und `/chat/completions`. | provider | base_url | Default-Modell | Key-Env | Quelle | |---------------|-----------------------------------|--------------------------|-----------------------|--------| | `openai` | https://api.openai.com/v1 | gpt-4o-mini | OPENAI_API_KEY | platform.openai.com | | `openrouter` | https://openrouter.ai/api/v1 | openai/gpt-4o-mini | OPENROUTER_API_KEY | openrouter.ai/docs | | `ollama` | http://localhost:11434/v1 | llama3.1 | – | ollama docs | | `dahl` | https://inference.dahl.global/v1 | MiniMaxAI/MiniMax-M2.7 | DAHL_API_KEY | https://docs.dahl.global/ | | `gonkarouter` | https://api.gonkarouter.io/v1 | MiniMaxAI/MiniMax-M2.7 | GONKAROUTER_API_KEY | https://gonkarouter.io/docs | | `custom` | (Pflicht) | (Pflicht) | – | – | **Dahl** (dahl.global) und **GonkaRouter** sind OpenAI-kompatible Inferenz-Dienste für offene Modelle auf dem dezentralen Gonka-GPU-Netz. Weitere Modelle (Stand 25.10.2026): `zai-org/GLM-5.3-Flash`, `deepseek-ai/DeepSeek-V4-Flash-0731`. Aktuelle IDs immer über `GET /models` prüfen. Bei Dahl braucht der Key ein zugewiesenes Token-Guthaben (sonst HTTP 402). ```yaml llm: provider: dahl # oder gonkarouter model: zai-org/GLM-5.3-Flash # optional ``` `GET /api/info` liefert Provider, Modell und base_url des Servers (ohne Key); die Android-App zeigt das bei „Verbindung testen" an. Test: ```bash curl localhost:8787/healthz curl -H "Authorization: Bearer $BYTECLAW_AUTH_TOKEN" -d '{"text":"Wie spät ist es in Berlin?"}' localhost:8787/api/chat ``` ### API | Methode | Pfad | Zweck | |---------|------------------------------------|------------------------------------------| | GET | /healthz | Health (ohne Auth) | | GET | /api/info | Version, Provider, Modell (ohne Secrets) | | GET | /api/sessions | Sessions (neueste zuerst) | | POST | /api/sessions | Session anlegen `{"title":""}` | | GET/PATCH/DELETE | /api/sessions/{id} | Lesen / umbenennen / löschen | | GET | /api/sessions/{id}/messages | Verlauf | | POST | /api/chat | Nicht-streamend `{"session_id"?, "text"}`| | GET | /api/ws | WebSocket-Streaming | Alle `/api/*` brauchen `Authorization: Bearer `. WebSocket: Client sendet `{"type":"message","session_id":"","text":"..."}`; Server sendet Events `session`, `delta`, `tool_call`, `tool_result`, `done` oder `error` (`{"type","text","tool","args","call_id","session_id"}`). `{"type":"ping"}` → `pong`. ### Build, Tests, Cross-Builds ```bash make # vet + test + build make race # Tests mit Race-Detector (benötigt CGO) make cross # dist/: linux-amd64, linux-arm64, linux-armv7, termux-arm64, termux-armv7 make docker ``` Termux: `byteclaw-termux-arm64` ist ein `GOOS=android`-Build; für armv7 wird ein statisches `linux/arm`-Binary gebaut, da `android/arm` ohne NDK/CGO nicht linkbar ist. In Termux z. B. nach `$PREFIX/bin` kopieren, `chmod +x` und starten. ### Docker ```bash docker build -t byteclaw . docker run -d -p 127.0.0.1:8787:8787 -v byteclaw-data:/data \ -e BYTECLAW_AUTH_TOKEN=... -e BYTECLAW_LLM_API_KEY=... byteclaw ``` Im Container lauscht der Server auf `0.0.0.0:8787`; nach außen nur hinter einem TLS-Reverse-Proxy (Caddy, Traefik) oder in einem VPN (z. B. Tailscale) freigeben. ## Android-Client ``` android/ ├─ app/src/main/java/dev/byteclaw/client/ │ ├─ MainActivity.kt Navigation (Chat, Sessions, Einstellungen) │ ├─ data/ByteClawApi.kt OkHttp REST + WebSocket-Flow │ ├─ data/SettingsStore.kt EncryptedSharedPreferences │ ├─ data/Models.kt kotlinx.serialization │ └─ ui/ ChatViewModel, Compose-Screens └─ gradle/libs.versions.toml Versionskatalog (AGP 8.7.3, Kotlin 2.0.21, compileSdk 35, minSdk 26) ``` Build (JDK 17 + Android SDK, `ANDROID_HOME` oder `local.properties` mit `sdk.dir`): ```bash cd android ./gradlew assembleDebug # app/build/outputs/apk/debug/app-debug.apk ./gradlew testDebugUnitTest ./gradlew assembleRelease # unsigniert; Signing-Config fehlt noch ``` In der App unter Einstellungen Server-URL (`https://…`; im Debug-Build auch `http://10.0.2.2:8787` für den Emulator) und Token eintragen, „Verbindung testen", speichern. ## Nächste Schritte - Release-Signing, App-Icon, CI (GitHub Actions) für Go + Android - Session-Kontext zusammenfassen statt nur Fenster der letzten N Nachrichten - Weitere Provider (Anthropic nativ), Abbrechen laufender Antworten über WebSocket - Optional: Einrichtung per QR-Code (URL + Token) ## Vorschau Screenshots mit Demo-Daten (Paparazzi, ohne Emulator) liegen in `preview/` (hell/dunkel). Neu erzeugen: `cd android && ./gradlew recordPaparazziDebug`.