Second, short-lived-connection transport next to the WS: same frames, same outbox/cursor, same token, served over plain HTTP (stdlib ThreadingHTTPServer bridged into the asyncio loop; zero new deps). - http_server.py: /v1/health (unauthenticated), POST /v1/frame (accept-and-ack; validation rejections as 4xx error frames), SSE /v1/events (outbox catch-up with id=cursor, event: hello, 15s heartbeat, bounded-queue backpressure), long-poll /v1/poll (25s hold). Bearer token + X-Iris-Device (same allowlist as WS hello), 64 KiB body cap, per-device rate limit, optional TLS, non-fatal bind failure. - ws_server.py: inbound dispatch chain extracted to shared dispatch_frame() used by both transports. - adapter.py: ANDROID_HTTP_PORT/CERT/KEY config; start/stop next to the WS; delivery counting in _broadcast_or_log (an SSE subscriber is a live subscriber -> no push, docs/19 19.8); _reply() routes point-to-point replies into the in-flight HTTP response (reply sink) or broadcasts when the device has no live WS (19.7); status/typing/ channel events fan out to both transports. - ws_probe.py: --http mode (health + POST + SSE turn drive, same assertion flags); tests/README updated. - Tests: hermes-agent/tests/gateway/test_android_http.py (23 tests, incl. the 19.8 delivery-counting regression); test_android.py (74) still green.
Iris × Hermes
A chat app for hermes-agent: a native Android app and a desktop app (Linux, macOS, Windows) — both built from one shared Kotlin codebase (Compose Multiplatform).
Iris pairs with your running hermes gateway over a private WebSocket and gives you a Telegram-quality chat experience with your personal agent: streaming replies, visible reasoning, structured tool activity, channels, threads, media, search, and push notifications.
Features
- Native Hermes-Gateway integration — your hermes → gateway → Iris app
- Absolute Privacy! — everything stays on your own infrastructure
- No file limit
- No character limit
- Full markdown support — tables, checkmarks, bold, inline code, code blocks + syntax highlighting…
- HTML Artifact Preview — agent-sent HTML/CSS/JS rendered in an in-app WebView
- All settings live in the app, not in hermes
config.yml! Change everything on the fly. - Channels — create channels to keep track of your reports, cronjobs, webhook calls
- Pin channels, rename them, change the color/icon
- Customize how your chat should look like — color? Check! Images? Check!
- Reasoning collapse/expandable in the chat bubble
- Change the default behavior of tool & reasoning verbosity
- Threads — create your own, or let the AI create them with a title
- Search messages from everywhere
How it works
hermes-agent ──> hermes gateway ──(WebSocket :8790)──> Iris app (Android / Desktop)
gateway-plugin/is a hermes platform plugin (android). It runs inside thehermes gatewayprocess and opens a WebSocket server the apps connect to. Zero new Python dependencies, zero hermes-core changes.app/is one Compose Multiplatform Gradle project::shared(KMP, most of the code),:androidApp(native Kotlin + Jetpack Compose client),:desktopApp(the same app, tweaked for a big screen).- The app is a first-class hermes messaging platform, so everything the gateway
already does just works: slash commands, cron delivery,
send_messagerouting, coexistence with Telegram/Discord/etc. - Push notifications: FCM (primary) or ntfy (fallback).
Build from source
Prerequisites
| Where | You need |
|---|---|
| Gateway host | hermes-agent with its venv (uv sync) |
| Android build machine | JDK 17, Android SDK (sdk.dir in app/local.properties or ANDROID_HOME), ADB with a connected device |
| Desktop build machine | JDK 17 only |
No system Gradle needed — both apps use the project wrapper (./gradlew).
1. Gateway (on the gateway host)
# hermes-agent is a separate project (not part of this repo)
cd hermes-agent && uv sync
# install the Iris plugin into the live hermes home
mkdir -p ~/.hermes/plugins
ln -s "$PWD/gateway-plugin" ~/.hermes/plugins/android
hermes gateway status # should list "android"
hermes gateway setup # generates ANDROID_TOKEN, prints the server URL
hermes gateway # run the gateway
2. Android app
cd app
./gradlew :androidApp:installDebug # build + install on the connected ADB device
# or just build the APK:
./gradlew :androidApp:assembleDebug # → app/androidApp/build/outputs/apk/debug/
3. Desktop app
cd app
./gradlew :desktopApp:run # dev run
./gradlew :desktopApp:jpackage # native app-image (bundles the JRE)
4. Pair
On the app's Connect screen:
- Server URL —
ws://<gateway-ip>:8790/ws(printed byhermes gateway setup). - Pairing token — from the setup output, or
ANDROID_TOKENin~/.hermes/.envon the gateway host. - Test & Connect.
Notes:
- The app has no QR scanner — pairing is manual URL + token entry.
- The default bind is
127.0.0.1(desktop on the same machine only). For a phone on the LAN, setANDROID_WS_HOSTto the gateway's LAN IP. - Remote access: Tailscale/WireGuard, or a reverse proxy with CA-signed WSS
(
ANDROID_WS_CERT/ANDROID_WS_KEY).
Full walkthrough, push setup (FCM/ntfy), and troubleshooting:
docs/setup.md.
Contributing
Contributions are welcome! Before you start:
-
Read the docs. The full reference library is in
docs/— start withdocs/00-overview.md, then follow the numbered docs (architecture, wire protocol, plugin design, testing, …). -
Know the layout.
Path What gateway-plugin/Python hermes platform plugin ( android);protocol.pyis the frame source of truthapp/sharedKMP module with most of the client code (shared by Android + Desktop) app/androidAppThin Android shell (package dev.iris.app)app/desktopAppThin desktop shell docs/Numbered reference library -
Run the tests.
- Python (gateway plugin):
cd hermes-agent && scripts/run_tests.sh tests/gateway/test_android.py(never barepytest— hermes's runner sandboxesHERMES_HOME). - Kotlin:
cd app && ./gradlew :shared:testDebugUnitTest - Live check (gateway must be running):
gateway-plugin/tests/ws_probe.pyandgateway-plugin/tests/e2e.py— seegateway-plugin/tests/README.md.
- Python (gateway plugin):
-
Keep the protocol in sync.
gateway-plugin/protocol.py,app/shared/.../protocol/Protocol.kt, anddocs/protocol/frames.schema.jsonmust always agree. -
Pre-commit hooks are configured (
.pre-commit-config.yaml); runpre-commit installonce after cloning.
Open an issue first for anything big, then send a pull request.
License
Apache License 2.0 — see LICENSE.