- IRIS_PUSH_BACKEND now defaults to ntfy (keeps push metadata on your own infrastructure); FCM is opt-in via IRIS_PUSH_BACKEND=fcm - build_push_backend(): ntfy for empty/unknown names, FCM only on explicit 'fcm' - gateway setup: warn when FCM is chosen (metadata routed via Google's servers) - README: privacy note + dedicated push section; new docs/playstore-listing.md with the FCM/ntfy privacy note for the Play Store listing - docs: 00/02/03/08/12/16 + setup.md updated to ntfy-default wording - tests: default-backend assertion updated (86/86 pass)
178 lines
7.1 KiB
Markdown
178 lines
7.1 KiB
Markdown
# Setup — Pairing a Device
|
|
|
|
User-facing guide: get a phone or desktop talking to your hermes gateway in
|
|
under 10 minutes. Design rationale lives in the numbered docs
|
|
([`09-pairing-security.md`](09-pairing-security.md),
|
|
[`08-push.md`](08-push.md), [`12-toolchain.md`](12-toolchain.md)); this page is
|
|
just the steps.
|
|
|
|
## Prerequisites
|
|
|
|
| Where | You need |
|
|
| --- | --- |
|
|
| Gateway host | hermes installed with its venv (`cd hermes-agent && uv sync`, see [`12-toolchain.md` §12.4](12-toolchain.md)) |
|
|
| Android build machine | JDK 17, Android SDK with `ANDROID_HOME` set (or `app/local.properties`), ADB with a connected device |
|
|
| Desktop build machine | JDK 17 only |
|
|
|
|
Gradle needs no system install — both apps use the project wrapper
|
|
(`./gradlew`). First-time machine setup: [`12-toolchain.md`](12-toolchain.md).
|
|
|
|
## 1. Gateway setup (on the gateway host)
|
|
|
|
Install the plugin into the live hermes home (dev: a symlink from the monorepo
|
|
root):
|
|
|
|
```bash
|
|
mkdir -p ~/.hermes/plugins
|
|
ln -s "$PWD/gateway-plugin" ~/.hermes/plugins/iris
|
|
hermes gateway status # should list "iris"
|
|
```
|
|
|
|
Run the interactive setup:
|
|
|
|
```bash
|
|
hermes gateway setup
|
|
```
|
|
|
|
What it does:
|
|
|
|
- Generates `IRIS_TOKEN` (64 hex chars) if none exists and stores it in
|
|
`~/.hermes/.env` (it prints the token once, at generation).
|
|
- Prompts for the WS bind host (default `127.0.0.1`), port (default `8790`),
|
|
and push backend (`ntfy` or `fcm`, default `ntfy`); warns when `fcm` is
|
|
chosen (push metadata via Google's servers).
|
|
- Prints the pairing payload (a QR-encodable `iris://pair?host=…&port=…&token=…`
|
|
string), a scannable QR of that payload, and the server URL
|
|
(`ws://<host>:8790/ws`).
|
|
|
|
Then start the gateway:
|
|
|
|
```bash
|
|
hermes gateway # or: hermes gateway restart after config changes
|
|
```
|
|
|
|
> **Note:** the default bind host `127.0.0.1` only accepts connections from the
|
|
> gateway host itself (e.g. a desktop app on the same machine). For a phone on
|
|
> the LAN, re-run `hermes gateway setup` (or edit `~/.hermes/.env`) and set
|
|
> `IRIS_WS_HOST` to the host's LAN IP (e.g. `192.168.1.10`).
|
|
|
|
## 2. Android app
|
|
|
|
Build and install (ADB device connected):
|
|
|
|
```bash
|
|
cd app
|
|
./gradlew :androidApp:installDebug
|
|
```
|
|
|
|
First run opens the **Connect** screen:
|
|
|
|
1. **Server URL** — `ws://<gateway-ip>:8790/ws` (the URL printed by
|
|
`hermes gateway setup`; use the LAN IP, not `127.0.0.1`, from a phone).
|
|
2. **Pairing token** — from the `hermes gateway setup` output, or
|
|
`grep IRIS_TOKEN ~/.hermes/.env` on the gateway host.
|
|
3. **Test & Connect** — performs a real `hello` (the auth leg), then saves the
|
|
pairing and connects.
|
|
|
|
> **Scan QR (Android):** the Connect screen has a **Scan QR** button (CameraX +
|
|
> ML Kit) that reads the QR printed by `hermes gateway setup` and pre-fills the
|
|
> URL + token. Desktop has no camera, so it uses manual entry. An `iris://pair`
|
|
> deep link (from any scanner) pre-fills the same way.
|
|
|
|
## 3. Desktop app
|
|
|
|
```bash
|
|
cd app
|
|
./gradlew :desktopApp:run # dev run
|
|
./gradlew :desktopApp:jpackage # native app-image (bundles the JRE)
|
|
```
|
|
|
|
Pairing is the same Connect screen (URL + token); the token is stored in the OS
|
|
keyring (with an encrypted-file fallback). Desktop push is tray icon + OS
|
|
notifications (no FCM).
|
|
|
|
> **Known issue:** on Linux with JDK 17 the jpackage launcher prints a
|
|
> non-fatal `pure virtual method called` warning (JDK-8348560, a
|
|
> jpackage/Linux launcher bug). The app runs and connects regardless.
|
|
|
|
## 4. Push notifications
|
|
|
|
Push wakes a backgrounded/offline device; on reconnect the app syncs the
|
|
outbox, so nothing is lost. Push fires when the device is offline, plus for
|
|
high-priority events (approvals, clarifies, cron) even when a device is live.
|
|
|
|
### ntfy (default; zero-config)
|
|
|
|
```
|
|
IRIS_PUSH_BACKEND=ntfy # the default — can be left unset
|
|
```
|
|
|
|
- The device **generates its own topic** automatically (no `NTFY_TOPIC` needed);
|
|
the server publishes to it.
|
|
- `NTFY_SERVER_URL` defaults to `https://ntfy.sh`. **Self-hosted ntfy is
|
|
recommended** — the public ntfy.sh SSE endpoint is flaky (it has served its
|
|
web UI instead of the stream), while a self-hosted instance gives reliable
|
|
SSE. For a real trust boundary use a private topic + `NTFY_AUTH_TOKEN`.
|
|
- **Privacy:** ntfy keeps push metadata (title, topic) on your own
|
|
infrastructure — this is the backend for truly private communication.
|
|
|
|
**What you see:** a low-priority foreground "ntfy listener" notification while
|
|
the app is off; incoming pushes trigger a silent sync.
|
|
|
|
### FCM (opt-in; needs a Firebase project)
|
|
|
|
> **Privacy note:** FCM push metadata (notification title, device token) is
|
|
> routed through **Google's servers**. If you want truly private
|
|
> communication, use ntfy (self-hosted) instead — it is the default.
|
|
|
|
1. Create a Firebase project (console.firebase.google.com) and add an Android
|
|
app with the app's applicationId; download `google-services.json` into
|
|
`app/androidApp/`.
|
|
2. Create a service account (Project settings → Service accounts → Generate new
|
|
private key) and store the JSON path in `~/.hermes/.env`:
|
|
`IRIS_FCM_SERVICE_ACCOUNT=/path/to/service-account.json`.
|
|
3. Set `IRIS_PUSH_BACKEND=fcm`.
|
|
|
|
Without a Firebase project the FCM path is **inert** (the app's FCM service
|
|
does nothing) — use ntfy (the default), or add Firebase later.
|
|
|
|
## 5. Remote access
|
|
|
|
- **Tailscale / WireGuard (recommended):** the gateway gets a stable tailnet IP;
|
|
the app connects to `ws://<tailnet-ip>:8790/ws`. No public exposure.
|
|
- **Reverse proxy / tunnel** (Caddy, Cloudflare Tunnel, ngrok): terminate TLS at
|
|
the edge, forward the WebSocket to `127.0.0.1:8790`.
|
|
- **WSS:** set `IRIS_WS_CERT` / `IRIS_WS_KEY` (paths, in
|
|
`~/.hermes/.env`) and the server serves `wss://` instead of `ws://`.
|
|
|
|
> **Honest limitation:** the app has **no certificate pinning** yet, so
|
|
> self-signed certs won't work — remote access requires **CA-signed** WSS for
|
|
> now. Plain `ws://` on a trusted LAN (or inside Tailscale) stays the default.
|
|
|
|
## 6. Troubleshooting
|
|
|
|
| Symptom | Likely cause / fix |
|
|
| --- | --- |
|
|
| `auth failed` / `error {code:"auth"}` on connect | Wrong token. Check `IRIS_TOKEN` in `~/.hermes/.env` on the gateway host (setup prints it only when it generates it). |
|
|
| Connection refused | Gateway not running (`hermes gateway status`); wrong URL (port `8790`, path `/ws`, LAN IP instead of `127.0.0.1` from a phone); firewall blocking the port. |
|
|
| Push not arriving | Backend not configured (gateway log: `push backend … not configured`); app backgrounded with no working backend; ntfy.sh SSE flakiness — use a self-hosted ntfy. |
|
|
| Desktop jpackage launcher warning | Non-fatal (JDK-8348560 on Linux JDK 17); the app runs and connects regardless. |
|
|
|
|
Smoke test without the app (from the gateway host):
|
|
|
|
```bash
|
|
python - <<'PY'
|
|
import asyncio, json, websockets
|
|
async def main():
|
|
async with websockets.connect("ws://127.0.0.1:8790/ws") as ws:
|
|
await ws.send(json.dumps({"v":1,"type":"hello","payload":{
|
|
"token":"<IRIS_TOKEN>","device_id":"test","device_name":"probe",
|
|
"caps":{"min_protocol":1}}}))
|
|
print("recv:", await ws.recv())
|
|
asyncio.run(main())
|
|
PY
|
|
```
|
|
|
|
Expect a `hello.ack`. If you get `error {code:"auth"}`, the token/host/port is
|
|
wrong.
|