- 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
116 lines
6.1 KiB
Markdown
116 lines
6.1 KiB
Markdown
# 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).
|