Chatlytics WhatsApp platform plugin for Hermes Agent (v0.14+)
Project description
chatlytics-hermes
Chatlytics WhatsApp platform plugin for Hermes Agent.
chatlytics-hermes connects Hermes to the Chatlytics WhatsApp gateway as a
first-class platform plugin. It registers a BasePlatformAdapter subclass,
21 Hermes tools (text + media + directory + sessions), an aiohttp inbound
webhook server, and a cron-delivery hook -- all auto-discovered by Hermes
through the hermes_agent.plugins entry-point group.
Status
Stable v3.0.0 release. Requires hermes-agent>=0.14,<0.15.
hermes-agent v0.14 is not yet on PyPI; install it from the GitHub tag
v2026.5.16 (see Install below). When v0.14 ships to PyPI the
install line simplifies to a plain pip install hermes-agent>=0.14.
Migration from 2.x
v3.0 is the first public PyPI release and carries three breaking changes that close every deferred breaking-change item from the v2.1 backlog. Tool-surface count unchanged at 21 tools.
If you call the MCP tool surface only:
chatlytics_get_chat_info-- callers checkingresult.get("success") is Falseto detect "chat not found" must now checkresult.get("chat") is None(success path). Error detection:result.get("success") is False and result.get("_error")surfaces the machine-readable code (transport_error,auth_error,server_error,validation_error,unknown_error).chatIdvalidation -- bare phone numbers and display names are now rejected at the schema layer. Resolve to a JID viachatlytics_searchfirst, then pass the@c.us/@g.us/@lid/@newsletterJID to chatId-bearing tools. Regex:/@(c\.us|g\.us|lid|newsletter)$/i.
If you call the Python adapter directly (library users):
adapter.send_image_file/send_animation_file/send_video_file/send_file_file-- removed. Useadapter.send_image(chat_id, resource=...)etc. Theresourceargument auto-detects URL vs local-path.Pathobjects, existing path strings,bytes, andbytearrayare all accepted. TheCHATLYTICS_UPLOAD_ALLOWED_ROOTSdefault-deny allowlist from v2.1 is preserved on every file branch.
See CHANGELOG.md ## [3.0.0] for the full breaking
list, additive items (smoke wheel caching, API audit doc), and
migration notes.
What's new in v3.0
Breaking-change harmonization. Three deferred breaking changes
from the v2.1 backlog ship in 3.0: chatlytics_get_chat_info return
shape disambiguates empty-vs-error with a machine-readable _error
sentinel code, chatId schemas tighten to JID-only (matches the
sibling JS bundle's regex), and the adapter's six send_*_file
methods collapse into unified send_*(resource=...) with
URL-vs-path auto-detection. Tool surface unchanged at 21 tools.
Additive. scripts/smoke.sh --cached caches the hermes-agent
wheel between smoke runs for faster local iteration.
.planning/HERMES-API-AUDIT.md inventories every hermes.* import
in the plugin so a future hermes 0.15 upgrade is fast.
Quality. Cosmetics sweep across adapter.py and tools.py
closes six explicitly-deferred LOW/INFO nits from the v2.1 audit
(docstring tightening, signature parity, module-level constants).
Zero behavior change. Test count: 120/120, preserved exactly.
What's new in v2.1
Security. v2.1.0 closes one BLOCKER and two HIGH issues carried
forward from the v2.0 milestone-wide review. BL-01 was a _keep_typing
async-cm vs base-coroutine shape mismatch that would have crashed the
plugin on the first production inbound message. HI-01 was an arbitrary
local-file read primitive on the 5 media tools (prompt-injectable
filePath="/etc/passwd"); v2.1 introduces a default-deny
CHATLYTICS_UPLOAD_ALLOWED_ROOTS env-configured path allowlist. HI-03
fixed **kwargs gaps on two media overrides for upstream-signature
forward-compat. All v2.0.0 callers should upgrade.
Quality. Live-loader integration smoke (tests/test_live_loader.py)
now exercises the real register(ctx) path against a respx-mocked
gateway and asserts all 21 tools land -- the test harness gap that hid
BL-01 is closed. Observability hardening: send_typing transport errors
log at DEBUG (not WARNING); dropped reserved metadata keys emit a WARN
per drop; silent ctx.get_platform failures now leave a DEBUG breadcrumb.
Test infra cleanup: conftest now teardown-clean, _FakePlatformConfig
consolidated into one shared fixture, scripts/smoke.sh --fast skips
docker for local iteration. The 21-tool surface is unchanged -- v2.1
is a drop-in upgrade from v2.0.
See CHANGELOG.md for the full Security / Added / Changed / Fixed / Docs breakdown.
Install
pip install "hermes-agent @ git+https://github.com/NousResearch/hermes-agent.git@v2026.5.16"
pip install chatlytics-hermes
pip install chatlytics-hermes pulls the latest 3.x from PyPI and
registers the chatlytics plugin under the hermes_agent.plugins
entry-point group, so Hermes discovers it automatically on next
gateway start.
For a development install from source:
git clone https://github.com/omernesh/chatlytics-hermes.git
cd chatlytics-hermes
pip install "hermes-agent @ git+https://github.com/NousResearch/hermes-agent.git@v2026.5.16"
pip install -e ".[dev]"
Configuration
Configure the plugin via environment variables (preferred) or a YAML config
block (hermes config edit).
Environment variables
| Variable | Required | Description |
|---|---|---|
CHATLYTICS_BASE_URL |
yes | Chatlytics gateway base URL (e.g. https://gateway.chatlytics.ai) |
CHATLYTICS_API_KEY |
yes | Bearer token for REST authentication |
CHATLYTICS_ACCOUNT_ID |
no | Default session/account ID for outbound sends |
CHATLYTICS_WEBHOOK_PORT |
no | Local port for the aiohttp inbound webhook listener (default: 8765) |
CHATLYTICS_WEBHOOK_SECRET |
no | HMAC-SHA256 shared secret for X-Chatlytics-Signature verification |
CHATLYTICS_HOME_CHANNEL |
no | Default chat_id for cron / notification delivery |
CHATLYTICS_UPLOAD_ALLOWED_ROOTS |
no | OS-pathsep-separated absolute paths that media tools may read from disk (default-deny when unset; see Security below) |
Security: filePath upload allowlist (CHATLYTICS_UPLOAD_ALLOWED_ROOTS)
The 5 media tools (chatlytics_send_image, chatlytics_send_voice,
chatlytics_send_video, chatlytics_send_file,
chatlytics_send_animation) accept an optional filePath parameter
that uploads a local file to the Chatlytics gateway. To prevent
prompt-injection or LLM-manipulation attacks from reading arbitrary
host files (e.g. /etc/passwd, C:\Windows\System32\config\SAM),
local-file uploads are default-deny: every filePath value is
rejected unless it resolves under a configured allowed root.
Configure the allowlist via CHATLYTICS_UPLOAD_ALLOWED_ROOTS, using
the OS path separator (: on POSIX, ; on Windows):
# POSIX
export CHATLYTICS_UPLOAD_ALLOWED_ROOTS="/var/lib/chatlytics/uploads:/tmp/chatlytics"
# Windows PowerShell
$env:CHATLYTICS_UPLOAD_ALLOWED_ROOTS = "C:\Users\Public\Documents\chatlytics;C:\Temp\chatlytics"
When CHATLYTICS_UPLOAD_ALLOWED_ROOTS is unset, every filePath
upload returns
{"success": false, "error": "Permission denied: Local file uploads are disabled; ..."}.
URL-based uploads via mediaUrl are unaffected — only the local-file
path is gated.
Recommended practice: point the allowlist at a dedicated upload
directory that is OS-owned (mode 0700), and pipe agent-produced
files through that directory before invoking a media tool.
YAML config (optional)
platforms:
chatlytics:
enabled: true
extra:
base_url: https://gateway.chatlytics.ai
api_key: ${CHATLYTICS_API_KEY}
account_id: 3cf11776_logan
webhook_port: 8765
home_channel: "120363100000000000@g.us"
Usage
Once installed, chatlytics is auto-registered as a Hermes platform plugin.
Start the gateway as usual:
hermes gateway start
To verify the plugin loaded, enumerate registered entry points:
python -c "import pkg_resources; print([ep.name for ep in pkg_resources.iter_entry_points('hermes_agent.plugins')])"
# -> [..., 'chatlytics', ...]
Send a text message from an agent toolset:
result = await ctx.tools.chatlytics_send(
chatId="120363100000000000@g.us",
text="Hello from Hermes",
)
# -> {"success": True, "messageId": "..."}
Tool catalog
Twenty-one tools are registered under the chatlytics toolset, grouped by
function. Full JSON schemas live in
src/chatlytics_hermes/tools.py.
Messaging (10)
chatlytics_send, chatlytics_reply, chatlytics_react, chatlytics_edit,
chatlytics_unsend, chatlytics_pin, chatlytics_unpin, chatlytics_read,
chatlytics_delete, chatlytics_poll.
Media (5)
chatlytics_send_image, chatlytics_send_voice, chatlytics_send_video,
chatlytics_send_file, chatlytics_send_animation. Each accepts either a
remote mediaUrl or a local filePath; local files are uploaded to the
gateway's /api/v1/upload endpoint first.
Directory / search (3)
chatlytics_directory, chatlytics_search, chatlytics_actions.
chatlytics_actions is read-only -- it issues a GET against the
gateway's action catalog and returns the list of dispatchable actions
with their schemas. Use it when an agent needs to discover what actions
are available before invoking one.
Sessions / health (3)
chatlytics_health, chatlytics_login, chatlytics_dispatch.
chatlytics_dispatch is the POST counterpart to chatlytics_actions
-- it invokes a generic gateway action by name with arbitrary payload.
Use the actions tool to discover, the dispatch tool to invoke. The split
is intentional and mirrors the Chatlytics Claude Code MCP bundle's
GET-vs-POST separation.
Every tool returns {"success": bool, ...}. On non-2xx responses or
transport errors the result is {"success": False, "error": "...", ...}
with the original status code and parsed body preserved.
Development
git clone https://github.com/omernesh/chatlytics-hermes.git
cd chatlytics-hermes
pip install "hermes-agent @ git+https://github.com/NousResearch/hermes-agent.git@v2026.5.16"
pip install -e ".[dev]"
pytest tests/
bash scripts/smoke.sh # dockerized clean-room verification
scripts/smoke.sh runs the package against a fresh python:3.13-slim
container -- it installs hermes-agent + this plugin in a clean Python
environment, asserts the chatlytics entry point is discoverable, then runs
the full test suite. Use this to validate a release before tagging.
For faster local iteration, pass --cached to cache the hermes-agent
wheel between runs at .smoke-cache/:
bash scripts/smoke.sh --cached
The first cached run downloads the wheel; subsequent runs install from
the local cache (no network). The cache invalidates automatically when
the pinned hermes-agent tag changes. If the cached install fails
(corrupted wheel, missing dep), the script falls back to a normal
network install and refreshes the cache.
Architecture notes
A few intentional design decisions, surfaced here so they don't surprise contributors:
- Inbound webhook lives inside
connect(). The aiohttp server starts whenadapter.connect()is called and stops ondisconnect(). There is no separate thread or process -- inbound webhook handling runs on the same event loop as outbound sends, which keeps message ordering deterministic and avoids cross-thread state coordination. - Outbound transport is
httpx.AsyncClientwith a 30 s default timeout. The client is created atconnect()and torn down atdisconnect(); tool handlers resolve it lazily throughctx.get_platform("chatlytics").adapter.client. _keep_typingis anasynccontext manager, not the base coroutine shape some upstream Hermes platforms use. The asynccontextmanager form composes cleanly withasync within tool handlers and guarantees the background heartbeat task is cancelled on exit even if the wrapped body raises. This shape divergence is intentional; the heartbeat fires immediately on enter (so the typing bubble appears without waiting the full 30 s interval) and re-fires every 30 s thereafter.- Local media files are read off the event loop. The local-path branch
of
_resolve_media_urlwrapsopen()+read()inasyncio.to_threadso concurrent media-tool calls don't stall the loop while a multi-MB file is read from disk. _keep_typingshape (v2.1). v2.1 rewrote_keep_typingas a plain coroutine matching the upstreamBasePlatformAdaptersignature(self, chat_id, interval=30.0, metadata=None, stop_event=None). The in-plugin async-cm ergonomics callers used in v2.0 are preserved via a new_typing_scope(chat_id)helper -- callers shouldasync with self._typing_scope(chat_id):instead ofasync with self._keep_typing(chat_id):. Public tool surface unchanged.
Known issues
filenamefor URL-path documents may not be honored by the gateway.chatlytics_send_fileaccepts afilenameparameter that the Chatlytics gateway is expected to surface as the saved filename on the recipient end. For local-path uploads, the filename is set when the bytes are POSTed to the gateway's upload endpoint, so it always takes effect. For URL-path documents (where the plugin only forwards amediaUrl), it is not yet confirmed that the gateway re-sets the filename downstream. Tracking upstream; if you rely on filename control, prefer the local-path mode and theCHATLYTICS_UPLOAD_ALLOWED_ROOTSallowlist.
License
MIT. See LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file chatlytics_hermes-3.0.0.tar.gz.
File metadata
- Download URL: chatlytics_hermes-3.0.0.tar.gz
- Upload date:
- Size: 80.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bc47e99ea9abf9eed6e7227f89d15ff06aecf9a7998f9619972be4116acc0b87
|
|
| MD5 |
21a7cd6d2eb228c5c4d31da5fbf8d955
|
|
| BLAKE2b-256 |
7c92c2ded8defc32a49ad5ec6555982fa93fbdd8ff67813f3d97886f383898c2
|
File details
Details for the file chatlytics_hermes-3.0.0-py3-none-any.whl.
File metadata
- Download URL: chatlytics_hermes-3.0.0-py3-none-any.whl
- Upload date:
- Size: 44.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2535f6a49719008b41223a79a5de0a0c06076bdbea57ced6903c678eac239839
|
|
| MD5 |
99d1099ccb5d4e481243903e40146798
|
|
| BLAKE2b-256 |
b1f84200077aa0b2b573d874f7cbff7b3b2b43bce03487104b480e65ccc836ca
|