hermes-conduit-android/README.md

117 lines
6.1 KiB
Markdown
Raw Normal View History

# Hermes Conduit — Android
Nativer Android-Client für einen self-hosted [Hermes Agent](https://hermes-agent.nousresearch.com)
über das Dashboard-Gateway. Android-Pendant zur iOS-App
[Hermes Conduit](https://github.com/kaishi00/hermes-conduit) (SwiftUI, MIT) — das RPC-Protokoll
ist 1:1 von dort portiert.
**UI-Sprache: Deutsch.** Dark Theme (Hermes-Look) ist Standard.
## Features (v1, MVP)
- **Login**: Host, Port, Benutzername, Passwort (Host/Port vorbefüllt mit `100.64.0.8` / `9119`).
Zugangsdaten werden verschlüsselt via `EncryptedSharedPreferences` (Android Keystore) gespeichert;
Auto-Login beim nächsten Start.
- **Session-Liste**: Titel, ID, Nachrichtenzahl, Suche, Pull-to-Reload via Refresh-Button,
neue Session anlegen.
- **Chat**: Streaming-Antworten (Live-Deltas), Markdown-Rendering mit Syntax-Highlighting
für Code-Blöcke, Tool-Aufruf-Zeilen, Abbruch-Button, Verlauf via `session.resume`.
- **Profile**: Anzeige und Wechsel der Hermes-Profile (`profiles.list`), Session-Liste lädt
profil-scoped neu.
- **Kontext-Auslastung**: Fortschrittsbalken im Chat-Header aus `session.usage`/`context.update`-Events.
Nicht in v1: Voice, Push, Kanban, Attachments/Bilder, OAuth, Approval/Clarify-Dialogs
(Server-Requests werden mit `-32601` abgelehnt, damit Tools nicht hängen).
## Build
Voraussetzungen: JDK 17, Android SDK (cmdline-tools, platform-35, build-tools 35.0.0).
```bash
export ANDROID_HOME=/root/android-sdk # oder eigenes SDK; alternativ local.properties mit sdk.dir
./gradlew assembleDebug
# APK: app/build/outputs/apk/debug/app-debug.apk
```
## Architektur
```
app/src/main/java/de/derittyp/hermesconduit/
├── core/ # Android-frei (reines JVM+OkHttp+kotlinx.serialization) → JVM-testbar
│ ├── AuthClient.kt # HTTP-Auth: password-login, /api/auth/me, ws-ticket, CookieJar
│ ├── HermesClient.kt # JSON-RPC 2.0 über WS-Textframes, Event-Parser, RPC-Methoden
│ ├── Models.kt # StreamEvent, SessionSummary, ChatMessage, RpcException
│ └── Json.kt # kotlinx.serialization Helfer
├── AppViewModel.kt # StateFlow-basierter UI-State, Event-Kollektion
├── CredentialStore.kt # EncryptedSharedPreferences (Keystore)
├── MainActivity.kt # Compose Navigation (login/sessions/chat)
└── ui/ # LoginScreen, SessionsScreen, ChatScreen, MarkdownMessage, Theme
```
## Auth-Flow (verifiziert gegen Live-Dashboard am 2026-10-21)
1. `POST /auth/password-login` — JSON `{username, password, provider:"basic"}`
→ `{"ok":true}` + `Set-Cookie: hermes_session_at, hermes_session_rt, hermes_session_provider`
2. `GET /api/auth/me` (mit Cookies) → `{"user_id":"...", ...}`
3. `POST /api/auth/ws-ticket` (mit Cookies) → `{"ticket":"...", "ttl_seconds":30}` (single-use)
4. WebSocket: `ws://<host>/api/ws?ticket=<ticket>`
Das Refresh-Cookie hat 30 Tage TTL; der Server refresht das Access-Cookie transparent.
Die App speichert v1 keine Cookies, sondern loggt bei Bedarf erneut ein (Credentials liegen
verschlüsselt vor). Ablauf der Session → Fehler in der UI → erneutes Einloggen.
## RPC-Protokoll (Port von iOS `Conduit/Services/HermesClient.swift`)
- Frames: JSON-RPC 2.0 **Textframes**. Client-Requests mit Integer-`id`; Responses matchen die `id`.
- Events: `{"method":"event","params":{"type":...,"session_id":...,"payload":{...}}}`
- Server→Client-Requests (`method` + String-`id`): v1 lehnt alle mit `-32601` ab.
- Auf `gateway.ready` antwortet der Client mit `client.capabilities {server_requests: true}`.
- Nicht-default-Profil: `profile`-Param wird an jede scoped RPC gehängt.
Genutzte Methoden: `session.list`, `session.resume` (`cols:96, source:"android", inline_images:false`),
`session.create`, `session.title`, `session.interrupt`, `prompt.submit` (`session_id, text, surface`),
`profiles.list`, `session.delete` (optional, nur zum Aufräumen von Test-Sessions).
**ID-Modell (live verifiziert):** `session.list` liefert die *stored* id (z. B. `20261021_130231_e692eb`),
`session.resume`/`session.delete` verlangen sie. `session.create` liefert beide
(`session_id` = kurze Runtime-id, `stored_session_id`). `prompt.submit`/`session.title` akzeptieren
die Runtime-id; die Resume-Antwort liefert sie als `session_id` zurück — die App nutzt nach dem
Resume die Runtime-id für Sends und zum Matchen von Streaming-Events. `session.delete` verweigert
aktive Sessions (4023) und doppelte Titel werden mit 4022 abgelehnt.
Streaming-Events: `message.start/delta/complete`, `tool.start/tool.complete`, `session.busy`,
`session.usage`/`context.update` (Kontext-Balken), `sessions.changed` (triggert Reload, 800 ms debounced).
## Live-Test
Der Netzwerk-Core ist bewusst Android-frei und damit auf der JVM gegen das echte
Dashboard testbar:
```bash
HERMES_TEST_PASSWORD='<passwort>' ./gradlew :app:testDebugUnitTest --tests '*LiveGatewayTest'
```
Der Test (ohne Env-Var automatisch übersprungen, Credentials nie im Repo):
login → me → ws-ticket → connect → `session.list` → `profiles.list` → `session.resume`
(readonly) → **neue Test-Session** `conduit-android-test` anlegen → eine kurze Nachricht senden
(startet den echten Agenten, kostet Tokens!) → Streaming-Antwort abwarten → Verlauf prüfen →
Test-Session löschen (falls der Gateway `session.delete` kann, sonst bleibt sie bestehen).
Zusätzlich Negativtest: falsches Passwort schlägt fehl.
**Wichtig:** Nie in bestehenden Sessions (z. B. WhatsApp-Hauptsession) Nachrichten senden.
## Bekannte TODOs für v2
- Cookie-Persistenz + transparentes Session-Refresh statt erneutem Passwort-Login
- Approval-/Clarify-Dialoge (Server→Client-Requests beantworten statt ablehnen)
- Attachments/Bilder (`image.attach_bytes`, REST-History `/api/sessions/{id}/messages`
für paginierte History statt Full-Resume)
- REST-History-Hydration wie iOS (bounded pages) statt komplettem Transcript im Resume
- Voice, Push-Benachrichtigungen, Kanban-Tab, OAuth (RFC 8252 Native Flow)
- Reconnect-Logik mit Backoff bei Socket-Verlust
- Release-Signing, adaptive Icons, Edge-to-Edge-Padding-Feinschliff
## Lizenz
MIT (wie die iOS-Referenz-App).