byteclaw/README.md

176 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.
- Shell-Tool standardmäßig AUS (`tools.shell.enabled`). Wenn an: nur Befehle aus `allowed_commands` (Interpreter/Netzwerk-/Shell-Programme wie `sh`, `bash`, `env`, `xargs`, `python`, `curl`, `git` sind nie erlaubt), jedes Glied von `|`, `&&`, `||`, `;` wird geprüft, `$`, Backticks, `<( )`, Umleitungen, `&`, Klammern werden abgelehnt, keine Pfade außerhalb des Workspace (`/`, `~`, `..`), Ausführung ohne `sh -c` direkt per exec im Workspace, Timeout, Output-Limit, bereinigte Umgebung (nur `PATH`, `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
```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 <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).
```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 |
| 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
```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 # 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.
```bash
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`.