Third-party XMPP/Jabber gateway platform plugin for Hermes Agent.
This packages the work from the upstream Hermes Agent XMPP PRs as an optional user plugin so self-hosters can use XMPP now instead of waiting for the core PR to land. It supports:
- 1:1 XMPP chats
- MUC group rooms
- mandatory STARTTLS to the XMPP server
- XEP-0085 typing indicators
- XEP-0363 HTTP file upload for media/files
- XEP-0004 data forms (e.g., clarify prompts)
- XEP-0050 ad-hoc commands
- XEP-0394 message markup (bold, code, lists)
- XEP-0461 threaded message replies
- XEP-0444 message reactions (e.g., 👀/✅/❌ for processing status)
- XEP-0447 stateless file sharing for voice messages
- OMEMO end-to-end encryption (optional, needs slixmpp-omemo)
- cron and
send_messagedelivery through a standalone sender hook - Hermes platform plugin registration via
ctx.register_platform(...)
Traffic is encrypted to your XMPP server with TLS. When OMEMO is enabled and slixmpp-omemo is installed, 1:1 messages are also end-to-end encrypted so the server cannot read content. MUC OMEMO support depends on client/device availability.
Most adapter code is derived from Eric Lars Lee's upstream PR #17469. Mibay's
PR #3105 is credited for earlier XMPP/OMEMO exploration. See ATTRIBUTION.md
for the full credit note. This repo exists because waiting is annoying, not
because the packager wants credit for other people's work. Tiny open-source goblin
energy, responsibly attributed.
From the Hermes profile you want to use:
cd ~/.hermes/plugins
git clone https://github.com/fastfinge/hermes-xmpp-plugin.git
uv pip install --python ~/.hermes/hermes-agent/venv/bin/python -r ~/.hermes/plugins/hermes-xmpp-plugin/requirements.txt
hermes config set plugins.enabled '["hermes-xmpp-plugin"]'If you run Hermes from a different checkout/venv, install requirements into that Python environment instead.
Restart the gateway after installing or changing env vars:
hermes gateway restartAdd these to ~/.hermes/.env for the active profile:
XMPP_JID=[email protected]
XMPP_PASSWORD=your-password
XMPP_ALLOWED_USERS=[email protected]
# Optional:
XMPP_HOST=example.org
XMPP_PORT=5222
# XMPP_DIRECT_TLS=true # force XEP-0368 direct TLS; auto-on when XMPP_PORT=5223
# XMPP_USE_IPV6=false # disable IPv6 (default: auto-detect a working global v6 route)
# XMPP_CONNECT_TIMEOUT_SECS=240 # default 180s; raise further on a slow/high-latency link
XMPP_MUC_ROOMS=[email protected]/hermes
XMPP_MUC_NICK=hermes
XMPP_HOME_CHANNEL=[email protected]XMPP_ALLOWED_USERS uses bare JIDs. MUC access is gated by room membership: if
the bot joins a room listed in XMPP_MUC_ROOMS, messages in that room are accepted.
For quick local testing only, set XMPP_ALLOW_ALL_USERS=true.
You can also use config.yaml:
plugins:
enabled:
- hermes-xmpp-plugin
xmpp:
enabled: true
jid: [email protected]
password: ${XMPP_PASSWORD}
allowed_users:
- [email protected]
muc_rooms:
- [email protected]/hermes
home_channel: [email protected]Env vars win when both are set.
Install the extra dependencies:
uv pip install --python ~/.hermes/hermes-agent/venv/bin/python slixmpp-omemo omemoThen set in ~/.hermes/.env:
XMPP_OMEMO_ENABLED=true
# Optional — defaults to ~/.hermes/xmpp_omemo.json
XMPP_OMEMO_STORAGE_PATH=/home/fastfinge/.hermes/xmpp_omemo.jsonOr in config.yaml:
xmpp:
omemo_enabled: true
omemo_storage_path: /home/fastfinge/.hermes/xmpp_omemo.jsonOn first connect, the adapter generates an OMEMO device key and publishes it to the server. This may take a few seconds — the adapter waits for initialization before sending encrypted messages. If the recipient hasn't published device keys yet, messages fall back to plaintext (with a log warning).
MUC OMEMO is supported when all participants have compatible devices, but group encryption reliability varies by client. 1:1 encryption is the primary use case.
XMPP is nice for profile isolation because you can create one JID per profile:
[email protected]for your profile[email protected]for Mom's profile[email protected]for Dad's profile
Each Hermes profile gets its own .env, memory, sessions, and gateway process.
Prosody needs modules for MUC, HTTP file upload, and the new XEPs. On Debian/Ubuntu:
sudo apt install prosody
sudo prosodyctl adduser [email protected]Enable/configure in Prosody:
muc— group roomshttp_file_share— file upload (XEP-0363)mod_groups— optional, for ad-hoc command roster
For ejabberd, enable MUC plus mod_http_upload. The new features (reactions,
replies, markup) use standard XMPP stanzas and should work on any modern server
supporting XEP-0444, XEP-0461, and XEP-0394.
xmpp_auth_failed: wrong JID/password, or server auth policy issue.xmpp_connect_timeout: the gateway will report the platform as connected right away (connecting doesn't block gateway startup), but a background watchdog gives up and retries if the session hasn't actually established withinXMPP_CONNECT_TIMEOUT_SECS(default 180s). A common cause on home networks: DNS returns both an A and AAAA record for the server, the IPv6 route is silently blackholed, and the OS burns through its full TCP retry budget (~130s on Linux defaults) before falling back to IPv4 — which then connects instantly. Checkpython3 -c "import socket; print(socket.getaddrinfo('your.server', 5222))"for a AAAA record, test IPv6 reachability separately (ping6/curl -6), and either fix IPv6 routing or disable IPv6 for this host if it's not actually usable. If raisingXMPP_CONNECT_TIMEOUT_SECSfurther fixes it, that confirms this cause. Otherwise it's a DNS/firewall/SRV/reachability issue — tryXMPP_HOST/XMPP_PORTor check the server is reachable at all.xmpp_connection_lost: an established session dropped unexpectedly (server restart, network blip); the gateway's reconnect watcher should retry.xmpp_lock: another gateway process is already logged into this JID; stop it first, or use a separate bot account per gateway.- HTTP upload failure: enable XEP-0363 on the server and check max file size.
- DM rejected: add your bare JID to
XMPP_ALLOWED_USERS. - MUC silent: add room to
XMPP_MUC_ROOMSand invite the bot.
When Hermes starts processing a request, the adapter sends a 👀 reaction. When it finishes, ✅ or ❌ is sent depending on success or error. Reactions are visible in clients that support XEP-0444 (Conversations, Dino, Gajim, etc.).
Reactions can also be sent by Hermes tools; the adapter maps the reaction to the
original message using slixmpp's xep_0444.
When you reply to a bot message, the adapter tracks the thread using XEP-0461 message references. Replies from Hermes are sent back into the same thread so context is preserved.
Markdown-like syntax in Hermes responses is converted to XEP-0394 message markup:
**bold**→ `bold`code`→ `code```block```→ `block
Clients that support XEP-0394 render this natively. Others get the raw text.
When Hermes needs clarification, the adapter sends an XEP-0004 data form instead of a plain text list. The form includes:
- A hidden
clarify_idfield - A list-single choice field with the available options
- An "Other (type your own answer)" option for free-form responses
The message body also includes a numbered text fallback so that clients which
do not render jabber:x:data forms in <message> stanzas (Conversations,
ConverseJS, older Gajim) still display actionable choices. Users on those
clients can reply with the number or option text as normal.
When a client does render the form and the user submits it, the adapter reads
the form response, extracts the clarify_id and selected answer, and resolves
the clarification back to Hermes automatically.
Disabling data forms: If you prefer to always use plain text clarify
(never send XEP-0004 forms), set the environment variable
XMPP_DISABLE_CLARIFY_FORMS=true or add disable_clarify_forms: true to your
XMPP platform config in config.yaml. This forces the numbered text fallback
path even when the xep_0004 plugin is registered.
Voice messages from Hermes are sent as XEP-0447 Stateless File Sharing (SFS) messages. The file is uploaded via XEP-0363 HTTP upload first, then a lightweight SFS reference is sent. This allows clients to preview metadata before downloading.
Voice calls (Jingle) are not yet supported because slixmpp does not include a Jingle RTP implementation.
The adapter registers ad-hoc commands on the bot's JID. Users can discover them
with their client's command list (e.g., /cmd in Conversations). Currently a
basic command list is exposed; future releases may add Hermes-specific commands.
The test suite runs standalone — no Hermes checkout required. It uses mocks for the Hermes gateway and slixmpp internals.
python -m pytest -qFor testing against a real Hermes checkout, set PYTHONPATH:
PYTHONPATH=/path/to/hermes-agent python -m pytest -q