A client that completes TCP but vanishes mid-TLS-handshake (e.g. a phone losing its network/VPN while traveling) blocked ssl.SSLSocket.accept() inside serve_forever forever: the gateway stopped accepting any new device connections (the app could not reconnect), and on the next restart httpd.shutdown() froze the whole event loop until the shutdown watchdog killed the process (ARIA journal 2026-09-11 / 2026-09-23). - Move the TLS handshake out of the accept loop: it now runs in the per-connection thread under a hard timeout (HANDSHAKE_TIMEOUT_S, 10 s); a failed/timed-out handshake just closes the socket. - stop() no longer blocks the event loop: shutdown()/server_close()/ join run in an executor under asyncio.wait_for(10 s); if the bound expires the daemon threads are abandoned. - Regression test: a silent half-open TCP connection must not stop fresh TLS connections from being served, and stop() must stay bounded.
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, token-authenticated connection 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! — chat stays on your own infrastructure
(push: ntfy by default, but the default ntfy server is the public
ntfy.sh— self-host ntfy to keep push metadata on your own machine; FCM is opt-in and routes push metadata via Google — see Push notifications) - 100 MB file uploads by default — configurable on the gateway via
max_upload_bytes(see Media §7.7); all limits are set on the gateway side (hermes), not in the app - No 4,096-character message limit like Telegram — messages travel over your own gateway (hard frame-body cap: 1 MiB)
- 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 ──(HTTP :8791)──> Iris app (Android / Desktop)
gateway-plugin/is a hermes platform plugin (android). It runs inside thehermes gatewayprocess and opens an HTTP 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: ntfy (default) or FCM (opt-in).
Push notifications
Push wakes a backgrounded/offline device; on reconnect the app syncs the outbox, so nothing is lost.
- ntfy (default) — the backend for truly private communication.
⚠️ By default it uses the public
https://ntfy.shcloud service — push metadata (topic, notification title) passes through ntfy.sh's servers. SetNTFY_SERVER_URLto a self-hosted ntfy to keep push metadata on your own infrastructure (recommended; public ntfy.sh SSE is also flaky). - FCM (opt-in,
IRIS_PUSH_BACKEND=fcm) — standard/reliable, but FCM push metadata (notification title, device token) is routed through Google's servers. If you want truly private communication, use ntfy instead.
Setup: docs/install.md; details:
docs/08-push.md.
Install the gateway
Three commands on the machine where hermes runs:
cd hermes-agent && uv sync # 1. hermes with its venv (separate project, not this repo)
# 2. install the Iris plugin — the #gateway-plugin suffix points the
# installer at the plugin subfolder of this monorepo
hermes plugins install git@gitea.zephyre.one:ARIA/iris_x_hermes.git#gateway-plugin
# 3. generate the pairing token + server URL, then run the gateway
hermes gateway setup
hermes gateway
hermes gateway setup prints the server URL and pairing token / QR
the app needs on its Connect screen.
All options (LAN binding, TLS, push, device allowlist) and the full app
pairing walkthrough: docs/install.md.
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)
If you're developing from a checkout, skip hermes plugins install and
symlink the plugin so it always tracks your working tree:
cd hermes-agent && uv sync
mkdir -p ~/.hermes/plugins
ln -s "$PWD/gateway-plugin" ~/.hermes/plugins/iris
hermes gateway status # should list the Iris platform
hermes gateway setup # generates IRIS_TOKEN, prints server URL + pairing QR
hermes gateway # run the gateway
(Otherwise see Install the gateway above.)
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 —
http://<gateway-ip>:8791(printed byhermes gateway setup). - Pairing token — from the setup output, or
IRIS_TOKENin~/.hermes/.envon the gateway host. - Test & Connect.
Notes:
- Android has a Scan QR button that reads the QR printed by
hermes gateway setupand pre-fills URL + token; desktop uses manual entry. - The default bind is
127.0.0.1(desktop on the same machine only). For a phone on the LAN, setIRIS_HTTP_HOSTto the gateway's LAN IP. - Remote access: Tailscale/WireGuard, or a reverse proxy/tunnel with TLS
(
IRIS_HTTP_CERT/IRIS_HTTP_KEY).
Full walkthrough, push setup (ntfy/FCM), TLS, and troubleshooting:
docs/install.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):
tests/ws_probe.pyandtests/e2e.py— seetests/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.