byteclaw/README.md

8 KiB
Raw Blame History

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

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

Alle /api/* brauchen Authorization: Bearer <token>. WebSocket: Client sendet {"type":"message","session_id":"<optional>","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

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      # 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)