# Hermes Matrix Client Integration Specification > **Issue**: [#166](http://143.198.27.163:3000/Timmy_Foundation/timmy-config/issues/166) — Stand up Matrix/Conduit > **Created**: Ezra | 2026-04-05 | Burn mode > **Purpose**: Define how Hermes wizard houses connect to, listen on, and respond within the sovereign Matrix fleet. This turns the #183 server scaffold into an end-to-end communications architecture. --- ## 1. Scope This document specifies: - The client library and runtime pattern for Hermes-to-Matrix integration - Bot identity model (one account per wizard house vs. shared fleet bot) - Message format, encryption requirements, and room membership rules - Minimal working code scaffold for connection, listening, and reply - Error handling, reconnection, and security hardening **Out of scope**: Server deployment (see `infra/matrix/`), room creation (see `scripts/bootstrap-fleet-rooms.py`), Telegram cutover (see `CUTOVER_PLAN.md`). --- ## 2. Library Choice: `matrix-nio` **Selected library**: [`matrix-nio`](https://matrix-nio.readthedocs.io/) **Why `matrix-nio`:** - Native async/await (fits Hermes agent loop) - Full end-to-end encryption (E2EE) support via `AsyncClient` - Small dependency footprint compared to Synapse client SDK - Battle-tested in production bots (e.g., maubot, heisenbridge) **Installation**: ```bash pip install matrix-nio[e2e] ``` --- ## 3. Bot Identity Model ### 3.1 Recommendation: One Bot Per Wizard House Each wizard house (Ezra, Allegro, Gemini, Bezalel, etc.) maintains its own Matrix user account. This mirrors the existing Telegram identity model and preserves sovereignty. **Pattern**: - `@ezra:matrix.timmytime.net` - `@allegro:matrix.timmytime.net` - `@gemini:matrix.timmytime.net` ### 3.2 Alternative: Shared Fleet Bot A single `@fleet:matrix.timmytime.net` bot proxies messages for all agents. **Not recommended** — creates a single point of failure and complicates attribution. ### 3.3 Account Provisioning Each account is created via the Conduit admin API during room bootstrap (see `bootstrap-fleet-rooms.py`). Credentials are stored in the wizard house's local `.env` (`MATRIX_USER`, `MATRIX_PASSWORD`, `MATRIX_HOMESERVER`). --- ## 4. Minimal Working Example The following scaffold demonstrates: 1. Logging in with password 2. Joining the fleet operator room 3. Listening for encrypted text messages 4. Replying with a simple acknowledgment 5. Graceful logout on SIGINT ```python #!/usr/bin/env python3 """hermes_matrix_client.py — Minimal Hermes Matrix Client Scaffold""" import asyncio import os import signal from pathlib import Path from nio import ( AsyncClient, LoginResponse, SyncResponse, RoomMessageText, InviteEvent, MatrixRoom, ) # ------------------------------------------------------------------ # Configuration (read from environment or local .env) # ------------------------------------------------------------------ HOMESERVER = os.getenv("MATRIX_HOMESERVER", "https://matrix.timmytime.net") USER_ID = os.getenv("MATRIX_USER", "@ezra:matrix.timmytime.net") PASSWORD = os.getenv("MATRIX_PASSWORD", "") DEVICE_ID = os.getenv("MATRIX_DEVICE_ID", "HERMES_001") OPERATOR_ROOM_ALIAS = "#operator-room:matrix.timmytime.net" # Persistent store for encryption state cache_dir = Path.home() / ".cache" / "hermes-matrix" cache_dir.mkdir(parents=True, exist_ok=True) store_path = cache_dir / f"{USER_ID.split(':')[0].replace('@', '')}_store" class HermesMatrixClient: def __init__(self): self.client = AsyncClient( homeserver=HOMESERVER, user=USER_ID, device_id=DEVICE_ID, store_path=str(store_path), ) self.shutdown_event = asyncio.Event() async def login(self): resp = await self.client.login(PASSWORD) if isinstance(resp, LoginResponse): print(f"✅ Logged in as {resp.user_id} (device: {resp.device_id})") else: print(f"❌ Login failed: {resp}") raise RuntimeError("Matrix login failed") async def join_operator_room(self): """Join the canonical operator room by alias.""" res = await self.client.join_room(OPERATOR_ROOM_ALIAS) if hasattr(res, "room_id"): print(f"✅ Joined operator room: {res.room_id}") return res.room_id else: print(f"⚠️ Could not join operator room: {res}") return None async def on_message(self, room: MatrixRoom, event: RoomMessageText): """Handle incoming text messages.""" if event.sender == self.client.user_id: return # Ignore echo of our own messages print(f"📩 {room.display_name} | {event.sender}: {event.body}") # Simple command parsing if event.body.startswith("!ping"): await self.client.room_send( room_id=room.room_id, message_type="m.room.message", content={ "msgtype": "m.text", "body": f"Pong from {USER_ID}!", }, ) elif event.body.startswith("!sitrep"): await self.client.room_send( room_id=room.room_id, message_type="m.room.message", content={ "msgtype": "m.text", "body": "🔥 Burn mode active. All systems nominal.", }, ) async def on_invite(self, room: MatrixRoom, event: InviteEvent): """Auto-join rooms when invited.""" print(f"📨 Invite to {room.room_id} from {event.sender}") await self.client.join(room.room_id) async def sync_loop(self): """Long-polling sync loop with automatic retry.""" self.client.add_event_callback(self.on_message, RoomMessageText) self.client.add_event_callback(self.on_invite, InviteEvent) while not self.shutdown_event.is_set(): try: sync_resp = await self.client.sync(timeout=30000) if isinstance(sync_resp, SyncResponse): pass # Callbacks handled by nio except Exception as exc: print(f"⚠️ Sync error: {exc}. Retrying in 5s...") await asyncio.sleep(5) async def run(self): await self.login() await self.join_operator_room() await self.sync_loop() async def close(self): await self.client.close() print("👋 Matrix client closed.") async def main(): bot = HermesMatrixClient() loop = asyncio.get_event_loop() for sig in (signal.SIGINT, signal.SIGTERM): loop.add_signal_handler(sig, bot.shutdown_event.set) try: await bot.run() finally: await bot.close() if __name__ == "__main__": asyncio.run(main()) ``` --- ## 5. Message Format & Protocol ### 5.1 Plain-Text Commands For human-to-fleet interaction, messages use a lightweight command prefix: | Command | Target | Purpose | |---------|--------|---------| | `!ping` | Any wizard | Liveness check | | `!sitrep` | Any wizard | Request status report | | `!help` | Any wizard | List available commands | | `!exec ` | Specific wizard | Route a task request (future) | | `!burn ` | Any wizard | Priority task escalation | ### 5.2 Structured JSON Payloads (Agent-to-Agent) For machine-to-machine coordination, agents may send `m.text` messages with a JSON block inside triple backticks: ```json { "hermes_msg_type": "task_request", "from": "@ezra:matrix.timmytime.net", "to": "@gemini:matrix.timmytime.net", "task_id": "the-nexus#830", "action": "evaluate_tts_output", "deadline": "2026-04-06T06:00:00Z" } ``` --- ## 6. End-to-End Encryption (E2EE) ### 6.1 Requirement All fleet operator rooms **must** have encryption enabled (`m.room.encryption` event). The `matrix-nio` client automatically handles key sharing and device verification when `store_path` is provided. ### 6.2 Device Verification Strategy **Recommended**: "Trust on First Use" (TOFU) within the fleet. ```python async def trust_fleet_devices(self): """Auto-verify all devices of known fleet users.""" fleet_users = ["@ezra:matrix.timmytime.net", "@allegro:matrix.timmytime.net"] for user_id in fleet_users: devices = await self.client.devices(user_id) for device_id in devices.get(user_id, {}): await self.client.verify_device(user_id, device_id) ``` **Caution**: Do not auto-verify external users (e.g., Alexander's personal Element client). Those should be verified manually via emoji comparison. --- ## 7. Fleet Room Membership ### 7.1 Canonical Rooms | Room Alias | Purpose | Members | |------------|---------|---------| | `#operator-room:matrix.timmytime.net` | Human-to-fleet command surface | Alexander + all wizards | | `#wizard-hall:matrix.timmytime.net` | Agent-to-agent coordination | All wizards only | | `#burn-pit:matrix.timmytime.net` | High-priority escalations | On-call wizard + Alexander | ### 7.2 Auto-Join Policy Every Hermes client **must** auto-join invites to `#operator-room` and `#wizard-hall`. Burns to `#burn-pit` are opt-in based on on-call schedule. --- ## 8. Error Handling & Reconnection ### 8.1 Network Partitions If sync fails with a 5xx or connection error, the client must: 1. Log the error 2. Wait 5s (with exponential backoff up to 60s) 3. Retry sync indefinitely ### 8.2 Token Expiration Conduit access tokens do not expire by default. If a `M_UNKNOWN_TOKEN` occurs, the client must re-login using `MATRIX_PASSWORD` and update the stored access token. ### 8.3 Fatal Errors If login fails 3 times consecutively, the client should exit with a non-zero status and surface an alert to the operator room (if possible via a fallback mechanism). --- ## 9. Integration with Hermes Agent Loop The Matrix client is **not** a replacement for the Hermes agent core. It is an additional I/O surface. **Recommended integration pattern**: ``` ┌─────────────────┐ │ Hermes Agent │ │ (run_agent) │ └────────┬────────┘ │ tool calls, reasoning ▼ ┌─────────────────┐ │ Matrix Gateway │ ← new: wraps hermes_matrix_client.py │ (message I/O) │ └────────┬────────┘ │ Matrix HTTP APIs ▼ ┌─────────────────┐ │ Conduit Server │ └─────────────────┘ ``` A `MatrixGateway` class (future work) would: 1. Run the `matrix-nio` client in a background asyncio task 2. Convert incoming Matrix commands into `AIAgent.chat()` calls 3. Post the agent's text response back to the room 4. Support the existing Hermes toolset (todo, memory, delegate) via the same agent loop --- ## 10. Security Hardening Checklist Before any wizard house connects to the production Conduit server: - [ ] `MATRIX_PASSWORD` is a 32+ character random string - [ ] The client `store_path` is on an encrypted volume (`~/.cache/hermes-matrix/`) - [ ] E2EE is enabled in the operator room - [ ] Only fleet devices are auto-verified - [ ] The client rejects invites from non-fleet homeservers - [ ] Logs do not include message bodies at `INFO` level - [ ] A separate device ID is used per wizard house deployment --- ## 11. Acceptance Criteria Mapping Maps #166 acceptance criteria to this specification: | #166 Criterion | Addressed By | |----------------|--------------| | Deploy Conduit homeserver | `infra/matrix/` (#183) | | Create fleet rooms/channels | `bootstrap-fleet-rooms.py` | | Verify encrypted operator-to-fleet messaging | Section 6 (E2EE) + MWE | | Alexander can message the fleet over Matrix | Sections 4 (MWE), 5 (commands), 7 (rooms) | | Telegram is no longer the only command surface | `CUTOVER_PLAN.md` + this spec | --- ## 12. Next Steps 1. **Gemini / Allegro**: Implement `MatrixGateway` class in `gateway/platforms/matrix.py` using this spec. 2. **Bezalel / Ezra**: Test the MWE against the staging Conduit instance once #187 resolves. 3. **Alexander**: Approve the command prefix vocabulary (`!ping`, `!sitrep`, `!burn`, etc.). --- *This document is repo truth. If the Matrix client implementation diverges from this spec, update the spec first.*