Skip to content
Β 
Β 

Latest commit

Β 

History

67 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ“¬ Zulip Plugin for Hermes

Python Tests Latest Release 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.


πŸš€ Quickstart β€” Running in 2 Minutes

1. Install the Zulip SDK (one-time)

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

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:

ZULIP_API_KEY=your-bot-api-key
ZULIP_EMAIL=your-bot@niyaz.zulipchat.com
ZULIP_SITE=https://niyaz.zulipchat.com

Then add to ~/.hermes/config.yaml:

gateway:
  platforms:
    zulip:
      enabled: true

4. Start

hermes gateway

Send a DM or @-mention your bot in a subscribed stream. Done! πŸŽ‰

For detailed setup, see docs/SETUP.md.
For admin configuration, see the 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:

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:

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 bot@company.zulipchat.com 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 present
Bot not responding in streams Bot must be subscribed to the stream in Zulip settings
"Invalid or unsafe ZULIP_SITE" Use https:// URL, not localhost or IP addresses
Setup wizard shows instructions only Ensure setup_fn=interactive_setup is passed to register()

For detailed agent instructions, see AGENTS.md.


πŸ”„ Updating

# One-command update (downloads latest + restarts Hermes)
ssh user@device "bash ~/.hermes/plugins/zulip/update.sh"

Or manually:

cd ~/.hermes/plugins/zulip
git pull origin main
hermes gateway restart

🀝 Contributing

# 1. Fork and clone
git clone https://github.com/YOU/zulip-hermes-integration.git
cd zulip-hermes-integration

# 2. Install hooks
bash scripts/setup-hooks.sh

# 3. Make changes
# ...

# 4. Run checks
bash .githooks/pre-push

# 5. Submit PR (squash merge, branch protection enforced)
  • 431 tests β€” run via pytest tests/
  • Pre-push hook β€” runs syntax checks + tests before every push
  • CI β€” GitHub Actions zulip-bridge job must pass before merge
  • Branch protection β€” requires PR + linear history + squash merge

πŸ“š See Also

License

MIT License β€” see LICENSE.

About

πŸ’¬ Zulip integration for Hermes Agent. Bi-directional chat, cron deliveries, and automated messaging with Zulip streams and DMs.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages