| android | ||
| cmd/byteclaw | ||
| internal | ||
| preview | ||
| site | ||
| site-preview | ||
| .dockerignore | ||
| .firebaserc | ||
| .gitignore | ||
| config.example.yaml | ||
| Dockerfile | ||
| firebase.json | ||
| go.mod | ||
| go.sum | ||
| Makefile | ||
| README.md | ||
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
- Konten:
POST /api/auth/register|login|logout,GET /api/auth/me; Passwörter als bcrypt-Hash, Tokens zufällig + gehasht gespeichert, 30 Tage gültig, Logout widerruft. Sessions sind pro Nutzer getrennt. Login-Rate-Limit pro IP.allow_registration(Standard an). Optionaler statischerauth_token(mind. 16 Zeichen) = Admin, sieht alle Sessions; 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 (ohneOrigin-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 perallow_private.- Datei-Tools sind per
os.Rootauf den Workspace beschränkt (kein.., keine absoluten Pfade, keine Symlink-Ausbrüche) und größenbegrenzt. - Shell-Tool standardmäßig AUS (
tools.shell.enabled). Wenn an: nur Befehle ausallowed_commands(Interpreter/Netzwerk-/Shell-Programme wiesh,bash,env,xargs,python,curl,gitsind nie erlaubt), jedes Glied von|,&&,||,;wird geprüft,$, Backticks,<( ), Umleitungen,&, Klammern werden abgelehnt, keine Pfade außerhalb des Workspace (/,~,..), Ausführung ohnesh -cdirekt per exec im Workspace, Timeout, Output-Limit, bereinigte Umgebung (nurPATH,HOME=Workspace,LANG,TERM– keine API-Keys/Token). - 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
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 <key> 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 <base_url>/models prüfen. Bei Dahl braucht der Key ein zugewiesenes Token-Guthaben (sonst HTTP 402).
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:
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 |
| POST | /api/sessions/{id}/cancel | Laufende Antwort abbrechen |
| POST | /api/sessions/{id}/summarize | Zusammenfassung manuell auslösen |
| GET | /api/sessions/{id}/summary | Letzte Zusammenfassung |
| GET | /api/providers | Provider-Liste + aktiv (ohne Keys) |
| PUT | /api/providers/active | Aktiven Provider/Modell setzen {"provider","model"?} |
| GET | /api/providers/{name}/models | Modellliste vom Provider (/v1/models) |
| PUT | /api/sessions/{id}/provider | Provider nur für diese Session ({"provider":""} = Standard) |
Alle /api/* brauchen Authorization: Bearer <token>.
WebSocket: Client sendet {"type":"message","session_id":"<optional>","text":"..."}; Server sendet Events
session, summarized, delta, tool_call, tool_result, done, cancelled oder error ({"type","text","tool","args","call_id","session_id"}). {"type":"ping"} → pong.
Abbrechen: {"type":"cancel","session_id":"…"} über denselben WebSocket oder POST /api/sessions/{id}/cancel. Der Abbruch läuft per Context bis in den HTTP-Request zum Provider; die bis dahin gestreamte Teilantwort wird als Assistant-Nachricht mit "cancelled":true gespeichert.
Provider umschalten
Alle Presets (openai, openrouter, ollama, dahl, gonkarouter) plus der in llm.* konfigurierte Provider stehen zur Auswahl. Keys kommen nur aus Env (DAHL_API_KEY, GONKAROUTER_API_KEY, …) oder llm.providers.<name>.api_key und verlassen den Server nie; die App sieht nur configured: true/false. Die Auswahl wird in SQLite gespeichert und überlebt Neustarts; pro Session kann ein anderer Provider gesetzt werden.
Lange Sessions
agent.summarize: ab max_messages (Standard 40) oder ~max_tokens (12000, Zeichen/4) noch nicht zusammengefasster Nachrichten fasst das aktive LLM die älteren Nachrichten zusammen (die neuesten keep_recent bleiben wörtlich, Schnitt immer vor einer User-Nachricht). Die Zusammenfassung liegt in der Tabelle summaries, die Originalnachrichten bleiben erhalten.
Build, Tests, Cross-Builds
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
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):
cd android
./gradlew assembleDebug # app/build/outputs/apk/debug/app-debug.apk
./gradlew testDebugUnitTest
./gradlew assembleRelease # signiert, wenn Keystore-Daten vorhanden sind (sonst unsigniert)
Release-Signing
Die signingConfig liest Keystore-Daten aus Umgebungsvariablen (BYTECLAW_KEYSTORE_FILE, BYTECLAW_KEYSTORE_PASSWORD, BYTECLAW_KEY_ALIAS, BYTECLAW_KEY_PASSWORD) oder aus android/keystore.properties (Vorlage: keystore.properties.example). keystore.properties, *.jks und *.keystore sind gitignored – Keystore niemals ins Repo.
mkdir -p ~/byteclaw-keys && chmod 700 ~/byteclaw-keys
keytool -genkeypair -keystore ~/byteclaw-keys/byteclaw-release.jks -storetype PKCS12 \
-alias byteclaw -keyalg RSA -keysize 3072 -validity 10000 -dname "CN=ByteClaw, C=DE"
cp android/keystore.properties.example android/keystore.properties # Passwörter eintragen
cd android && ./gradlew assembleRelease
$ANDROID_HOME/build-tools/35.0.0/apksigner verify --print-certs app/build/outputs/apk/release/app-release.apk
Keystore und Passwörter sicher sichern: Ohne denselben Schlüssel lassen sich Updates nicht installieren.
App-Icon
Adaptive Icon als Vektor (res/drawable/ic_launcher_{background,foreground,monochrome}.xml, mipmap-anydpi-v26): drei Krallen-Kratzer in Claw-Orange plus Byte-Bits in Türkis auf dunklem Navy – dieselben Markenfarben wie im Compose-Theme (ui/Theme.kt, res/values/colors.xml). Da minSdk 26 ist, deckt der Vektor alle Dichten ab; die Monochrom-Ebene sorgt für Themed Icons ab Android 13. Vorschau: preview/icon.png.
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
- CI (GitHub Actions) für Go + Android
- Weitere Provider (Anthropic nativ)
- App: Provider pro Session wählen (API existiert schon)
- Optional: Einrichtung per QR-Code (URL + Token)
Vorschau
Screenshots mit Demo-Daten (Paparazzi, ohne Emulator) liegen in preview/ (hell/dunkel): Chat mit Stopp-Button, Sessions, Einstellungen, Provider-Auswahl, icon.png. Neu erzeugen: cd android && ./gradlew recordPaparazziDebug.