ARIA 7faaf2aa1c
CI / Gateway plugin tests (push) Successful in 5m5s
CI / Kotlin tests (android host + desktop) (push) Successful in 6m50s
Per-device tokens with revocation (issue #11)
Auth previously used the shared IRIS_TOKEN as the security principal:
a leaked token meant access to all devices, and a compromised device
could not be isolated.

Gateway:
- pairing.py: devices.token column (in-place migration) + revoked
  denylist table; issue_token (idempotent, 64 hex), token_for,
  reissue_token, revoke/unrevoke/is_revoked/list_revoked. The token
  never leaks into device dicts (push fan-out / listings).
- http_server.py: auth accepts the shared token (bootstrap/legacy) OR
  the device's own token (both constant-time); a revoked device_id is
  rejected with 401 before either comparison. On SSE open (pairing)
  the per-device token is minted and returned in hello.ack.
- protocol.py: hello_ack(..., device_token).
- adapter.py: setup flow (hermes gateway setup -> Iris) now offers
  'Remove a paired device?' on an existing setup: numbered select
  menu (last option = exit the removal loop), confirmation, back to
  the menu for further removals.
- tools/iris_devices.py: operator CLI (list / revoke / unrevoke /
  reissue), stdlib only.

App:
- SecureStore.deviceToken (Android: EncryptedSharedPreferences;
  Desktop: second keyring slot iris-device-token / device_token.enc).
- HelloAckPayload.deviceToken; GatewayClient stores it on hello and
  presents it instead of the shared token from then on (live provider
  in HttpGateway); savePairing/clear wipe it for re-pairing.

Docs: 09 §9.3 stretch -> implemented (revocation semantics, both
control surfaces), 04 hello.ack example, frames.schema.json, M7 row 13.

Tests: 8 new Python tests (issuance, acceptance, revocation,
isolation, unrevoke, registry unit x2, setup-flow menu) - 94/94 pass;
2 new Kotlin wire tests - green. Live-verified against a running
gateway (hello.ack token matches devices.db; revoke -> 401 even with
shared token; unrevoke -> 200; setup TUI both paths).
2026-08-24 19:37:44 +02:00

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 (push: ntfy by default; FCM is opt-in and routes push metadata via Google — see Push notifications)
  • 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 the hermes gateway process 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_message routing, 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) — push metadata stays on your own infrastructure (self-hosted ntfy recommended). This is the backend for truly private communication.
  • 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/setup.md §4; details: docs/08-push.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)

# 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:

  1. Server URL — ws://<gateway-ip>:8790/ws (printed by hermes gateway setup).
  2. Pairing token — from the setup output, or ANDROID_TOKEN in ~/.hermes/.env on the gateway host.
  3. 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, set ANDROID_WS_HOST to 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 (ntfy/FCM), and troubleshooting: docs/setup.md.

Contributing

Contributions are welcome! Before you start:

  1. Read the docs. The full reference library is in docs/ — start with docs/00-overview.md, then follow the numbered docs (architecture, wire protocol, plugin design, testing, …).

  2. Know the layout.

    Path What
    gateway-plugin/ Python hermes platform plugin (android); protocol.py is the frame source of truth
    app/shared KMP module with most of the client code (shared by Android + Desktop)
    app/androidApp Thin Android shell (package dev.iris.app)
    app/desktopApp Thin desktop shell
    docs/ Numbered reference library
  3. Run the tests.

    • Python (gateway plugin): cd hermes-agent && scripts/run_tests.sh tests/gateway/test_android.py (never bare pytest — hermes's runner sandboxes HERMES_HOME).
    • Kotlin: cd app && ./gradlew :shared:testDebugUnitTest
    • Live check (gateway must be running): gateway-plugin/tests/ws_probe.py and gateway-plugin/tests/e2e.py — see gateway-plugin/tests/README.md.
  4. Keep the protocol in sync. gateway-plugin/protocol.py, app/shared/.../protocol/Protocol.kt, and docs/protocol/frames.schema.json must always agree.

  5. Pre-commit hooks are configured (.pre-commit-config.yaml); run pre-commit install once after cloning.

Open an issue first for anything big, then send a pull request.

License

Apache License 2.0 — see LICENSE.

S
Description
Iris × Hermes — native Android + Desktop client for hermes-agent via a gateway platform plugin. Compose Multiplatform app (Kotlin) + Python WebSocket gateway adapter. See docs/ for the full reference library.
Readme Apache-2.0
7.8 MiB
0 Stars 1 Watchers 0 Forks
Latest
2026-08-31 21:44:41 +00:00
Languages
Kotlin 52.2%
Python 47.4%
Shell 0.4%