Conversation
Adds XMPP as a bundled platform plugin under `plugins/platforms/xmpp/`,
giving Hermes feature parity with the other 1:1 messaging adapters
(SimpleX, IRC, LINE). The adapter requires zero core edits — slixmpp
is imported lazily, `Platform("xmpp")` is auto-discovered via the
bundled-plugin scan in `gateway/config.py`, and all hooks (setup,
env-enablement, cron delivery, standalone send, allowlist) are wired
through `register_platform()` kwargs.
Plugin contract:
- `check_requirements()` requires XMPP_JID + XMPP_PASSWORD + slixmpp
- `validate_config()` / `is_connected()` accept env or extra input
- `_env_enablement()` seeds PlatformConfig.extra (jid/host/port/tls/
rooms/nickname + home_channel) so env-only setups show up in
`hermes gateway status` without instantiating the client
- `_standalone_send()` opens an ephemeral session for cron deliveries
that run separately from the gateway
- `interactive_setup()` provides a stdin wizard for `hermes setup gateway`
- `register()` wires the adapter into the registry with required_env,
cron_deliver_env_var, allowed_users_env, install_hint, emoji, and a
platform_hint for the LLM (plain text, no markdown, MUC by nickname).
Behavior:
- 1:1 chat over `<message type='chat'>`, replies sent the same way.
- MUC rooms join with XMPP_NICKNAME and only reply when addressed
(matches IRC's convention); MUC targets use a `muc:` prefix in the
routing surface so `send_message(target="xmpp:muc:room@conf", ...)`
works.
- Bare JID allowlist via XMPP_ALLOWED_USERS (resources stripped before
the auth check); XMPP_ALLOW_ALL_USERS=true bypass for dev.
- STARTTLS on by default (XMPP_FORCE_STARTTLS); explicit warning when
disabled.
- Profile isolation via `acquire_scoped_lock("xmpp", bare_jid)` so two
profiles cannot log in as the same account.
- Markdown stripped before send, control characters dropped to keep
the XML 1.0 body valid, long messages chunked under a configurable
byte ceiling.
Lazy dependency: slixmpp is imported inside `connect()` and inside
the standalone sender; the plugin is importable and discoverable even
when slixmpp is missing — `check_requirements()` returns False until
`pip install slixmpp` is run. No pyproject extras are introduced.
Environment variables:
XMPP_JID Bare JID (required)
XMPP_PASSWORD Account password (required)
XMPP_HOST Host override (default: SRV lookup)
XMPP_PORT Port override (default: 5222)
XMPP_FORCE_STARTTLS Require TLS negotiation (default: true)
XMPP_NICKNAME MUC nickname (default: JID local part)
XMPP_ROOMS Comma-separated MUC JIDs to auto-join
XMPP_ALLOWED_USERS Allowlisted bare JIDs
XMPP_ALLOW_ALL_USERS Dev-only escape hatch
XMPP_HOME_CHANNEL Default cron delivery target
XMPP_HOME_CHANNEL_NAME Human label for the home channel
Validation:
- `python -m pytest tests/gateway/test_xmpp_plugin.py` → 45 passed
- Combined sibling plugin run
(test_xmpp_plugin + test_simplex_plugin + test_irc_adapter +
test_line_plugin + test_platform_registry + test_plugins +
test_config) → 352 passed
- End-to-end plugin discovery + `Platform("xmpp")` enum lookup +
`register_platform()` wiring verified against `discover_plugins()`.
Brings the XMPP plugin closer to feature parity with WhatsApp / Telegram by
adding native attachment delivery and rich-text formatting, both
implemented as slixmpp plugins so no new pyproject dependencies are
introduced.
XEP-0363 HTTP File Upload
- Register ``xep_0363`` + ``xep_0066`` on connect.
- ``_resolve_upload_service`` pre-warms the upload-component JID on
session_start (or honors a pinned ``XMPP_UPLOAD_SERVICE``); the result
is cached so per-send discovery doesn't happen.
- Override ``send_image_file``, ``send_document``, ``send_voice``,
``send_video``, and ``send_animation`` to route local files through
``_upload_then_send`` → ``upload_file`` → OOB stanza
(``<x xmlns='jabber:x:oob'><url>...</url></x>``). Clients that
understand XEP-0066 render the attachment inline; clients that don't
still see the URL in ``<body>``.
- ``UploadServiceNotFound`` / ``FileTooBig`` / ``HTTPError`` are caught
and converted to a sticky local fallback: a text bubble describing
the file with the upload-failure reason — the same shape the base
class default uses.
- ``send_image`` (URL form) attaches an OOB extension for ``https://``
URLs so inline ```` images in the agent's response render
natively when the host is reachable.
XEP-0071 XHTML-IM
- Register ``xep_0071`` on connect (gated by ``XMPP_HTML_FORMATTING``).
- ``_markdown_to_xhtml_im`` converts to a safe XHTML subset
(``<strong>``, ``<em>``, ``<code>``, ``<pre>``, ``<a>``, ``<ul>``,
``<ol>``, ``<li>``, ``<blockquote>``) using the ``markdown`` library
when available, with a regex fallback otherwise — same dual path the
Matrix adapter uses for ``org.matrix.custom.html``. URLs are sanitized
to reject ``javascript:`` / ``data:`` / ``vbscript:`` schemes.
- ``send()`` emits the html block only for single-chunk messages and
only when the rendered fragment differs from a bare escape of the
plain body. Chunked sends fall back to plain text to avoid splitting
XHTML at arbitrary byte boundaries.
New environment variables
XMPP_UPLOAD_SERVICE Pin the upload component JID
XMPP_HTML_FORMATTING Disable XHTML-IM emission (default true)
Platform hint updated so the agent knows it can emit ``MEDIA:`` tags and
markdown freely; setup wizard, plugin.yaml, and the user-facing doc gain
matching guidance.
Validation:
- ``python -m pytest tests/gateway/test_xmpp_plugin.py`` →
66 passed (21 new cases covering the HTML converter, dual body,
upload-success path, upload fallback, pinned-service shortcut, new env
enablement seeds).
- Sibling-plugin regression sweep (xmpp + simplex + irc + line +
platform_registry + plugins + config) → 373 passed.
- End-to-end discovery + ``Platform("xmpp")`` enum lookup +
``XMPP_UPLOAD_SERVICE`` adapter wiring verified against
``discover_plugins()``.
|
Followup: addressed the two deferred items (HTTP Upload + XHTML-IM) called out in the original PR notes. The branch now has a second commit What's new
Notes for review
ValidationEnd-to-end discovery + No new pyproject dependencies — |
|
Thanks for the heads-up — I missed both prior PRs in my pre-flight search and I appreciate you flagging them. Closing this one in favor of #17469 (which @ericlarslee has had production-tested for almost a month and already has a community APPROVE) is the right call. Three small things from this branch that I think would be useful as targeted follow-ups on top of #17469 once it lands, rather than as a competing PR:
The branch with all three is preserved on the fork ( |
|
Closing in favor of #17469 (@ericlarslee), which got there first, has the broader 16-point integration, and already has a community APPROVE waiting on a maintainer merge. Per @alt-glitch's coordination ask, deferring rather than competing. Preserved on the fork for reference / mining:
Apologies for the duplicate review noise — I missed the existing PRs in my pre-flight search. |
|
Closing — superseded by #17469. See thread for context. |
What does this PR do?
Adds XMPP as a bundled platform plugin under
plugins/platforms/xmpp/, giving Hermes feature parity with the other 1:1 messaging adapters (SimpleX, IRC, LINE, WhatsApp). The adapter requires zero core edits — slixmpp is imported lazily,Platform("xmpp")is auto-discovered via the bundled-plugin scan ingateway/config.py, and all hooks (setup, env-enablement, cron delivery, standalone send, allowlist) are wired throughregister_platform()kwargs the way the sibling plugins do it.XMPP is an open, federated chat protocol. The plugin works against any self-hosted (Prosody, ejabberd, Snikket) or hosted server, supports 1:1 chats and MUC rooms, attaches files natively via XEP-0363 HTTP Upload, renders rich text via XEP-0071 XHTML-IM, and integrates with cron-job delivery and the
send_messagetool out of the box.Related Issue
No tracking issue; this is a new platform addition that matches an existing extension pattern.
Type of Change
Changes Made
plugins/platforms/xmpp/adapter.py—XMPPAdapter, env-enablement, standalone send, interactive setup, registration. slixmpp imported lazily. HTTP Upload viaxep_0363, OOB hints viaxep_0066, XHTML-IM viaxep_0071.plugins/platforms/xmpp/__init__.py— re-exportsregister.plugins/platforms/xmpp/plugin.yaml— manifest withrequires_env+optional_enventries surfaced by the config UI (includingXMPP_UPLOAD_SERVICEandXMPP_HTML_FORMATTING).tests/gateway/test_xmpp_plugin.py— 66 unit tests covering enum discovery, requirements gating, env enablement, init paths, helpers, send routing, inbound dispatch (including own-echo / unaddressed-MUC filtering), XHTML-IM markdown conversion (including URL-scheme sanitization and HTML-escaping inside code spans), single-chunk vs multi-chunk dual-body behavior, HTTP Upload success + fallback paths, pinned-service shortcut, standalone send guards, andregister()metadata.website/docs/user-guide/messaging/xmpp.md— full setup guide (Prosody quickstart, env vars, allowlist, HTTP Upload + XHTML-IM behavior, cron delivery, limitations).website/docs/user-guide/messaging/index.md— Next Steps link.website/docs/reference/environment-variables.md— XMPP_* var reference.scripts/release.py— author map entry for contributor email.Design
User-visible: operator sets
XMPP_JID,XMPP_PASSWORD,XMPP_ALLOWED_USERS(and optionallyXMPP_ROOMS) in~/.hermes/.env, runshermes gateway start. 1:1 chats from allowlisted JIDs reach the agent; MUC messages reach the agent only when prefixed with the bot's nickname (hermes: …), matching IRC's convention. Cron jobs target this platform viadeliver=xmpporsend_message(target="xmpp:…"). MUC targets use amuc:prefix in the routing surface.The agent can emit markdown (rendered via XHTML-IM dual body for opt-in clients, with plain-text fallback for the rest) and
MEDIA:/path/to/file(uploaded via XEP-0363 to the server's upload component, delivered with an XEP-0066 OOB extension so clients render the attachment inline). Servers without an upload component fall back to a clear "📎 filename (upload failed: …)" text line.Alternatives considered:
asyncio.open_connection+ SSL (IRC's approach). XMPP needs XML-stream/SASL/STARTTLS negotiation, stanza dispatch, MUC nick handling, presence and stream resumption — reimplementing that correctly is hundreds of lines of subtle protocol code and we'd own every CVE. Rejected.check_requirements()gate SimpleX uses forwebsockets).Edge cases covered: missing credentials → adapter never constructed; TLS disabled emits a warning at connect time; MUC self-echo filtered on
mucnick; unaddressed room chatter ignored; auth allowlist strips JID resources before lookup; CR/LF/NUL rejected in standalone-sendchat_id; profile isolation viaacquire_scoped_lock("xmpp", bare_jid)so two profiles cannot share an account; control characters dropped from message bodies so the XML 1.0 serializer never chokes; HTTP Upload service discovery cached so per-send latency is one IQ + one HTTP PUT, with sticky text fallback onUploadServiceNotFound/FileTooBig/HTTPError; XHTML-IM only emitted for single-chunk responses (multi-chunk falls back to plain text to avoid splitting XHTML across stanzas);javascript:/data:/vbscript:URL schemes rejected by the link sanitizer.Limitations (documented in the setup guide):
cache_image_from_urlinto the OOB / stanza-extension parsers.How to Test
Manual E2E (against a local Prosody container):
Then DM the bot from any XMPP client (Dino, Gajim, Conversations) registered as
you@localhost. Try sending markdown (**bold**) and ask the agent to share a file withMEDIA:/path/to/file.png.Backward compatibility
New platform — no public API, on-disk format, or wire-format changes.
Platform("xmpp")was previously rejected by_missing_(); existing configurations are unaffected.Notes for reviewers
slixmpp(BSD, lazy-imported, nopyproject.tomladdition). Matches the SimpleX/websocketspattern — the plugin is discoverable even when the package is absent. Users install it explicitly via the documentedpip install slixmpp. The XEP-0363 and XEP-0071 wiring is all done through slixmpp's bundled XEP plugins, so no further deps either.XMPP_FORCE_STARTTLSswitch maps to slixmpp'senable_starttlsandenable_direct_tlsinstance attributes. There is no true "refuse-plaintext" enforcement in slixmpp's public surface; the env name keeps continuity with operator expectations and the falsy path logs a warning. If you'd prefer a different name (XMPP_USE_TLS/XMPP_TLS), happy to rename.muc:chat-id prefix is plugin-local and only matters for the routing surface (send_message,_resolve_target). It does not leak into the platform wire format.markdownlibrary when available (already pinned for thematrixextra) and falls back to a regex pipeline matching the pattern Matrix uses fororg.matrix.custom.html.