Integration
Zulip Hermes Integration
Self-hosted Zulip plugin that makes an AI agent a full teammate in threaded team chat (streams + DMs) - the Hermes (Nous Research) adapter. Sovereign and self-hosted, no chat-vendor lock-in. Sibling of openclaw-zulip-bridge in a single Zulip agent family for open-source frameworks.
Install
pip install "zulip>=0.9.0"
Configuration Example
gateway:
platforms:
zulip:
enabled: true
README
# ๐ฌ Zulip Plugin for Hermes
[](https://python.org)
[](https://github.com/niyazmft/zulip-hermes-integration/actions)
[](https://github.com/niyazmft/zulip-hermes-integration/releases/latest)
[](LICENSE)
**Connect your Hermes AI agent to Zulip.** Chat with Hermes via **streams** (with automatic topic threading) or **DMs**. Supports admin commands, secure DM policies, file uploads, and health monitoring.
> ๐ก **What this does:** Your Zulip bot becomes a doorway to your Hermes AI. Users type in Zulip, the AI thinks, the bot replies โ all while keeping conversations threaded by topic.
> ๐ **Part of a single Zulip adapter family for open-source AI agents.**
> This repo is the **Hermes (Nous Research)** adapter. Its sibling,
> [`openclaw-zulip-bridge`](https://github.com/niyazmft/openclaw-zulip-bridge), does the
> same thing for the **OpenClaw** agent (TypeScript). Same thesis, two runtimes:
> bring a self-hosted AI agent into threaded, topic-first Zulip chat as a full teammate โ
> sovereign, no chat-vendor lock-in. The pattern Slack and Block's Buzz are racing to
> productize, delivered **open source** and **self-hosted**.
---
## ๐ Quickstart โ Running in 2 Minutes
### 1. Install the Zulip SDK (one-time)
```bash
pip install "zulip>=0.9.0"
```
> โ ๏ธ Hermes doesn't auto-install plugin dependencies. Run this once in the same Python environment as Hermes.
### 2. Install the Plugin
```bash
mkdir -p ~/.hermes/plugins
rm -rf ~/.hermes/plugins/zulip
git clone https://github.com/niyazmft/zulip-hermes-integration.git ~/.hermes/plugins/zulip
hermes plugins enable zulip
```
### 3. Configure
Add to `~/.hermes/.env`:
```bash
ZULIP_API_KEY=your-bot-api-key
[email protected]
ZULIP_SITE=https://niyaz.zulipchat.com
```
Then add to `~/.hermes/config.yaml`:
```yaml
gateway:
platforms:
zulip:
enabled: true
```
### 4. Start
```bash
hermes gateway
```
Send a DM or @-mention your bot in a subscribed stream. Done! ๐
**For detailed setup**, see [docs/SETUP.md](docs/SETUP.md).
**For admin configuration**, see the [Environment Variables](#environment-variables) section below.
---
## โจ What You Get
### For End Users
| Feature | What it does |
|---------|-------------|
| ๐ฌ **Streams + DMs** | Talk to the bot in public streams (with topic threading) or private messages |
| ๐ค **"Thinking..." placeholder** | Bot shows it's working, then edits with the final answer. No awkward silence. |
| ๐ **File uploads** | Send CSVs, PDFs, JSON โ the bot downloads and can process them |
| ๐ **Admin commands** | Type `/help`, `/status`, `/model`, `/streams`, `/user`, `/pin`, `/unpin` for instant responses (no LLM call needed) |
### For Admins
| Feature | What it does |
|---------|-------------|
| ๐ **DM Policies** | Control who can DM: `open`, `allowlist`, `pairing` (code-based onboarding), or `disabled` |
| ๐ฆ **Rate limiting** | Per-sender sliding-window rate limiter (default 60 msg/min) prevents message floods |
| ๐ **Audit logging** | Persistent JSON-line audit log with rotation for security forensics |
| ๐ฉบ **Health probe** | Pre-flight server check with SSRF protection + structured `health_status` logging |
| ๐ก๏ธ **Security hardening** | SSRF validation, symlink rejection (O_NOFOLLOW), path traversal blocking, TOCTOU-free file ops |
| โก **Performance caching** | LRU client + target caches + connection pooling (10 connections, retry on 5xx) |
| ๐ **Context metadata** | Every message carries `conversation_turn`, `session_gap_seconds`, `topic_changed` to help the AI avoid stale responses |
| ๐ **One-command updates** | `bash ~/.hermes/plugins/zulip/update.sh` pulls latest and restarts |
### For Developers
| Feature | What it does |
|---------|-------------|
| ๐ **Pure plugin** | Zero changes to Hermes core. Drop in, enable, done. |
| ๐งฉ **Extensible commands** | Add custom bot commands with `@register_command` decorator |
| ๐ **Sandboxed workspace** | Bot can generate files (reports, JSON, CSV) in a temp workspace with auto-cleanup |
| ๐งช **CI-tested** | 431 tests, pre-push hooks, GitHub Actions branch protection |
---
## ๐ Built-in Commands
Type these in any stream or DM. They're handled instantly โ no LLM call:
| Command | Response |
|---------|----------|
| `/help` | List all available commands |
| `/status` | Bot version, repo URL, your email |
| `/model` | Current model status |
| `/streams` | List streams (or ask AI for management) |
| `/user` | Get user info (or ask AI) |
| `/pin` | Star/pin a message (or ask AI) |
| `/unpin` | Unstar/unpin a message (or ask AI) |
Add your own:
```python
from zulip.commands import register_command
@register_command("ping")
def _cmd_ping(args, chat_id, sender_email, sender_name):
return "๐ Pong!"
```
---
## ๐ DM Access Control
Set `ZULIP_DM_POLICY` to control who can message the bot:
| Mode | Behavior | Use case |
|------|----------|----------|
| `open` *(default)* | Anyone can DM | Small teams, public bots |
| `allowlist` | Only `ZULIP_ALLOWED_USERS` can DM | Internal team bots |
| `pairing` | New users get a pairing code to share with an admin | Moderated onboarding |
| `disabled` | All DMs blocked | Stream-only bots |
**Pairing mode flow:**
```
New user DM โ "Your pairing code: PAIR-ABC123"
Admin approves โ user can DM normally
```
---
## ๐ Sending Files
The bot can generate and send files as Zulip uploads:
```python
from zulip.workspace import BotWorkspace
ws = BotWorkspace()
path = ws.save_text("report.csv", "id,value\n1,42\n")
await adapter.send(
chat_id="dm:42",
content="Here is your report:",
media_files=[path]
)
```
Files appear as clickable links. Temp files auto-delete after upload. Path traversal and symlinks are rejected.
---
## ๐๏ธ Architecture
```
Zulip Stream/DM
โ
ZulipAdapter._listen_for_events() # Event queue long-polling
โ
MessageEvent (with topic metadata + context fields)
โ
Gateway session โ AI Agent
โ
ZulipAdapter.send() โ Zulip REST API
```
All synchronous SDK calls are wrapped with `asyncio.to_thread()` to keep the gateway event loop responsive.
---
## ๐ง Environment Variables
### Required
| Variable | Example | Description |
|----------|---------|-------------|
| `ZULIP_API_KEY` | `abcd1234...` | Bot API key from Zulip settings |
| `ZULIP_EMAIL` | `[email protected]` | Bot email address |
| `ZULIP_SITE` | `https://company.zulipchat.com` | Your Zulip organization URL |
### Optional โ Access Control
| Variable | Default | Description |
|----------|---------|-------------|
| `ZULIP_ALLOWED_USERS` | *(empty)* | Comma-separated emails allowed to DM |
| `ZULIP_DM_POLICY` | `open` | `open` / `allowlist` / `pairing` / `disabled` |
| `ZULIP_GROUP_POLICY` | `open` | Group/stream policy: `open` / `allowlist` / `disabled` |
| `ZULIP_GROUP_ALLOW_FROM` | *(empty)* | Comma-separated emails allowed for stream messages |
| `ZULIP_MAX_MESSAGES_PER_MINUTE` | `60` | Per-sender rate limit (0 to disable) |
### Optional โ Behavior
| Variable | Default | Description |
|----------|---------|-------------|
| `ZULIP_CHATMODE` | `onmessage` | Stream trigger: `onmessage` / `oncall` / `onchar` |
| `ZULIP_REQUIRE_MENTION` | `true` | Stream messages need @mention (except `onmessage`) |
| `ZULIP_EDIT_PLACEHOLDER` | `true` | Show "Thinking..." placeholder while AI generates |
| `ZULIP_REACTIONS_ENABLED` | `true` | Emoji reactions (๐/โ
/โ ๏ธ) for status |
| `ZULIP_CHUNK_LIMIT` | `4000` | Max chars per message chunk |
| `ZULIP_TOPIC_SESSIONS` | `false` | Per-topic conversation sessions (opt-in) |
| `ZULIP_DM_SESSION_TURN_LIMIT` | `20` | DM session rotation after N turns (0 to disable) |
| `ZULIP_TYPING_DELAY_SECONDS` | `2.0` | Typing indicator delay after send |
| `ZULIP_STREAMS` | `*` | Comma-separated stream names to monitor |
| `ZULIP_RESPONSE_PREFIX` | *(empty)* | Prepended to every outbound message |
| `ZULIP_STREAM_OVERRIDES` | *(empty)* | JSON object mapping stream names to per-stream chatmode overrides |
#### How mentions are detected
In `oncall` and `onchar` modes the bot only replies when mentioned, so getting
this right matters.
Detection prefers Zulip's own `mentioned` flag, which the server sets for a
personal mention regardless of which markup the sender used. Text matching is
only a fallback for events that arrive without flags, and it recognises:
| Form | Where it comes from |
|---|---|
| `@Soju` | what `@**Soju**` becomes after inbound HTML/markdown stripping |
| `@**Soju**` | raw Zulip mention markup |
| `@_**Soju**` | silent mention |
| `@**Soju\|12**` | mention disambiguated by user id |
| `@soju-bot` | hand-typed email local-part |
Both the bot's display name and its email local-part are matched, because Zulip
writes mentions from the **display name** while the account is identified by the
local-part.
### Optional โ Advanced
| Variable | Default | Description |
|----------|---------|-------------|
| `ZULIP_CHUNK_MODE` | `length` | Chunking strategy: `length` or `newline` |
| `ZULIP_ONCHAR_PREFIXES` | `!,>` | Custom onchar triggers |
| `ZULIP_BLOCK_STREAMING` | `false` | Experimental block streaming |
| `ZULIP_MEDIA_MAX_MB` | `5` | Max inbound attachment size (MB) |
| `ZULIP_ALLOW_ALL_USERS` | `false` | Disable all authorization (dev only) |
| `ZULIP_CONNECT_TIMEOUT` | `30` | Connection timeout (seconds) |
| `ZULIP_READ_TIMEOUT` | `60` | Read timeout (seconds) |
| `ZULIP_SEND_TIMEOUT` | `90` | Send timeout (seconds) |
---
## ๐ Troubleshooting
| Problem | Fix |
|---------|-----|
| "zulip package not installed" | Run `pip install "zulip>=0.9.0"` in Hermes's Python env |
| "No adapter available for zulip" | Check logs for syntax errors; verify `plugin.yaml` is presen
... (truncated)
integration
Comments
Sign in to leave a comment