docs+plugin: HTTP-only transport cleanup, install guide, review fixes
CI / Gateway plugin tests (push) Successful in 5m19s
CI / Kotlin tests (android host + desktop) (push) Successful in 7m3s

- docs/install.md: new end-to-end guide for non-technical users
  (gateway install, app install, LAN/TLS/remote connection, push,
  options, troubleshooting); docs/setup.md now points to it
- README: new 'Install the gateway' section; pairing section updated
  for HTTP transport (8791, QR scan on Android)
- rename IRIS_WS_HOST -> IRIS_HTTP_HOST (clean rename, no compat
  fallback); drop dead DEFAULT_PORT=8790
- setup.py: advertise https:// in the printed/QR server URL when
  IRIS_HTTP_CERT is set
- ws_probe.py/e2e.py: default --url http://127.0.0.1:8791, env
  IRIS_WS_URL -> IRIS_HTTP_URL, honor explicit port + https scheme
- plugin.yaml: IRIS_HTTP_* env names, description no longer says
  'WebSocket server'
- docs 03/09/12/19: fix stale WS-era refs (ws_server.py cites,
  8790 smoke test, WSS->HTTPS, 'HTTP fallback' reframed as the
  only transport)
- AGENTS.md: symlink name android -> iris (matches actual install)
- test: adapter reads IRIS_HTTP_HOST/CERT/KEY from env; legacy
  IRIS_WS_* names are not consulted (95/95 pass)
This commit is contained in:
ARIA committed 2026-08-24 22:22:02 +02:00
1 parent a61b47a947
commit b1c9bac7d8
18 files changed
+472 -317

No files matched your search

+36 -36
View File
@@ -27,13 +27,13 @@ requires_env:
prompt: "Iris pairing token"
password: true
optional_env:
- name: IRIS_WS_HOST
description: "WS bind host (default 127.0.0.1; use 0.0.0.0 for LAN)"
prompt: "WS host"
- name: IRIS_HTTP_HOST
description: "HTTP bind host (default 127.0.0.1; use 0.0.0.0 for LAN)"
prompt: "HTTP host"
password: false
- name: IRIS_WS_PORT
description: "WS port (default 8790)"
prompt: "WS port"
- name: IRIS_HTTP_PORT
description: "HTTP port (default 8791)"
prompt: "HTTP port"
password: false
- name: IRIS_HOME_CHANNEL
description: "Default chat id for cron/notification delivery (default default)"
@@ -67,13 +67,13 @@ optional_env:
description: "ntfy server URL (default https://ntfy.sh)"
prompt: "ntfy server URL"
password: false
- name: IRIS_WS_CERT
description: "TLS cert path for WSS (optional)"
prompt: "WSS cert"
- name: IRIS_HTTP_CERT
description: "TLS cert path for HTTPS (optional)"
prompt: "HTTPS cert"
password: false
- name: IRIS_WS_KEY
description: "TLS key path for WSS (optional)"
prompt: "WSS key"
- name: IRIS_HTTP_KEY
description: "TLS key path for HTTPS (optional)"
prompt: "HTTPS key"
password: false
```
@@ -215,27 +215,26 @@ The adapter tracks per-chat **turn state** (in-turn, current streaming
`message` vs `tool.*` vs `commentary`. The exact classification markers are
verified empirically in M2 (see `13-testing.md`).
## 3.4 WebSocket server (`ws_server.py`)
## 3.4 HTTP server (`http_server.py`)
- Library: **`websockets`** (core dep, v15). `websockets.serve(handler, host,
port, ssl=ctx)`.
- **Handler** per connection:
1. Await first frame; must be `hello {token, device_id, device_name, caps,
fcm_token?}`. Verify token (constant-time) + allowlist. On failure: send
`error {code:"auth"}` and close.
2. On success: register in connection registry
(`device_id → {ws, caps, fcm_token}`), send
`hello.ack {server_caps, sync_cursor, channels[]}`.
3. Loop: decode frames, dispatch to adapter inbound handlers.
4. On close: deregister; if no devices remain, ensure pending outbox
frames have push fired.
- Library: **stdlib `http.server`** (`ThreadingHTTPServer` +
`BaseHTTPRequestHandler`) in a daemon thread; bridges into the gateway's
asyncio loop via `asyncio.run_coroutine_threadsafe`. Optional TLS via
`ssl.SSLContext` (`IRIS_HTTP_CERT`/`IRIS_HTTP_KEY`). Full design:
`19-http-fallback-transport.md`.
- **Auth:** `Authorization: Bearer <token>` (constant-time `verify_token`)
- device allowlist via `X-Iris-Device`; `401` on failure.
- **Endpoints:** `GET /v1/health` (unauthenticated liveness),
`POST /v1/frame` (any JSON frame the protocol accepts),
`GET /v1/events?cursor=N` (SSE: outbox catch-up + live frames),
`GET /v1/poll?cursor=N` (long-poll fallback), `POST /v1/media` +
`GET /v1/media/{id}` (media upload/pull).
- **Routing:** `emit(chat_id, frame)` → broadcast to **all** connected
devices (no per-chat subscribe; single-user model). Global frames
(`channel.*`, `status`) also broadcast to all.
- **Heartbeat:** WS ping/pong + app-level `ping`/`pong`; dead peers reaped.
- **Backpressure:** per-connection send queue with a bounded buffer; drop
`message.update` (coalesce to latest) under pressure, never drop
`message`/`tool.end`/`notification`.
- **Limits:** 64 KiB request body cap, per-device token-bucket rate limit
(20/s, burst 40) → `429`; media uploads bounded by the per-upload total
cap. No CORS (app clients only).
## 3.5 State & storage (all under `get_hermes_home()/"iris"`)
@@ -253,18 +252,19 @@ verified empirically in M2 (see `13-testing.md`).
## 3.6 Config resolution
- **Secrets (`.env`):** `IRIS_TOKEN`, `IRIS_FCM_SERVICE_ACCOUNT`,
`IRIS_FCM_SERVER_KEY`, `IRIS_WS_CERT/KEY`, `NTFY_TOPIC` (if secret).
`IRIS_FCM_SERVER_KEY`, `IRIS_HTTP_CERT/KEY`, `NTFY_TOPIC` (if secret).
- **Behavioral (`config.yaml` → `gateway.platforms.iris.extra`):** `host`,
`port`, `home_channel`, `allowed_users`, `push_backend`, `outbox_retention_hours`,
`max_upload_bytes`, `tls`.
`http_port`, `home_channel`, `allowed_users`, `push_backend`, `outbox_retention_hours`,
`max_upload_bytes`, `http_cert`/`http_key`.
- Env vars override `config.yaml` (hermes convention). Read secrets with the
scope-aware `_get_scoped_secret` pattern (see `plugins/platforms/irc/adapter.py:42`)
so multiplexed profiles don't leak each other's tokens.
## 3.7 Failure & lifecycle safety
- WS server bind failure → `_set_fatal_error("bind_failed", …, retryable=True)`.
- All outbound sends are best-effort; a dead socket latches and the frame falls
to the outbox.
- `disconnect()` cancels the server task and closes sockets cleanly.
- HTTP server bind failure → non-fatal: log a warning, disable the HTTP leg,
show it in the inspector (the plugin keeps working for other platforms).
- All outbound sends are best-effort; a dead stream latches and the frame
falls to the outbox.
- `disconnect()` stops the HTTP server and closes streams cleanly.
- Token/PII redaction in all logs (hermes PII policy).