- Kotlin + Jetpack Compose (Material 3, Dark Theme), minSdk 26, target 35 - Auth-Flow wie iOS Conduit: password-login → /api/auth/me → ws-ticket → WS - JSON-RPC 2.0 über WS-Textframes, 1:1 portiert aus Conduit/Services/HermesClient.swift - Streaming (message.delta/complete), Markdown mit Code-Highlighting (mikepenz) - Profile-Anzeige/-Wechsel, Kontext-Auslastungs-Balken aus session.usage - Zugangsdaten via EncryptedSharedPreferences (Android Keystore) - Netzwerk-Core Android-frei → JVM-Live-Test gegen das echte Dashboard |
||
|---|---|---|
| app | ||
| gradle/wrapper | ||
| .gitignore | ||
| build.gradle.kts | ||
| gradle.properties | ||
| gradlew | ||
| gradlew.bat | ||
| README.md | ||
| settings.gradle.kts | ||
Hermes Conduit — Android
Nativer Android-Client für einen self-hosted Hermes Agent über das Dashboard-Gateway. Android-Pendant zur iOS-App 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 viaEncryptedSharedPreferences(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).
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)
POST /auth/password-login— JSON{username, password, provider:"basic"}→{"ok":true}+Set-Cookie: hermes_session_at, hermes_session_rt, hermes_session_providerGET /api/auth/me(mit Cookies) →{"user_id":"...", ...}POST /api/auth/ws-ticket(mit Cookies) →{"ticket":"...", "ttl_seconds":30}(single-use)- 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 dieid. - Events:
{"method":"event","params":{"type":...,"session_id":...,"payload":{...}}} - Server→Client-Requests (
method+ String-id): v1 lehnt alle mit-32601ab. - Auf
gateway.readyantwortet der Client mitclient.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:
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}/messagesfü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).