# 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:///api/ws?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='' ./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).