← Back to Plugins
Integration

Zulip Hermes Integration

niyazmft By niyazmft ⭐ 9 stars 👁 9 views ▲ 0 votes

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.

GitHub

Install

pip install "zulip>=0.9.0"

Configuration Example

gateway:
  platforms:
    zulip:
      enabled: true

README

# ๐Ÿ“ฌ Zulip Plugin for Hermes

[![Python](https://img.shields.io/badge/python-3.8%2B-blue)](https://python.org)
[![Tests](https://img.shields.io/badge/tests-431%20passing-brightgreen)](https://github.com/niyazmft/zulip-hermes-integration/actions)
[![Latest Release](https://img.shields.io/github/v/release/niyazmft/zulip-hermes-integration?label=release)](https://github.com/niyazmft/zulip-hermes-integration/releases/latest)
[![License](https://img.shields.io/badge/license-MIT-green)](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

Loading comments...