Per-device tokens with revocation (issue #11)
CI / Gateway plugin tests (push) Successful in 5m5s
CI / Kotlin tests (android host + desktop) (push) Successful in 6m50s

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).
This commit is contained in:
ARIA committed 2026-08-24 19:37:44 +02:00
1 parent 746d809d48
commit 7faaf2aa1c
23 files changed
+837 -66

No files matched your search

+4 -1
View File
@@ -50,6 +50,7 @@ iris_x_hermes/
## Module responsibilities
### `gateway-plugin/` (Python)
- **`plugin.yaml`** — manifest: `name: iris-platform`, `kind: platform`,
`requires_env` / `optional_env` (surfaced in `hermes config`/setup).
- **`adapter.py`** — `IrisAdapter(BasePlatformAdapter)` + `register(ctx)`.
@@ -68,6 +69,7 @@ iris_x_hermes/
- **`search.py`** — FTS5 query bridge over the hermes session store.
### `app/shared` (Kotlin KMP)
- **`commonMain`** — protocol models (kotlinx-serialization), `GatewayClient`
(OkHttp WS), repositories (Room), ViewModels (StateFlow), and the Compose UI
(design system, screens). ~80% of app code.
@@ -77,6 +79,7 @@ iris_x_hermes/
window management, `MediaPlayer` actual.
### `app/androidApp` / `app/desktopApp`
Thin shells: `Application`/`MainActivity` (Android) and `main()`/window
(Desktop). They compose the `shared` UI and inject platform services.
@@ -127,4 +130,4 @@ keystore.jks
- **Plugin:** `~/.hermes/plugins/iris/` ← copy of `gateway-plugin/`
(or a symlink for dev). Discovered by hermes's `PluginManager`.
- **App (dev):** installed on-device via `./gradlew :androidApp:installDebug`.
- **App (desktop, dev):** `./gradlew :desktopApp:run`.
- **App (desktop, dev):** `./gradlew :desktopApp:run`.