Files
zulip-platform-plugins/docs/PRD.md
T

12 KiB

Product Requirements Document: Zulip Platform Plugins

Status: Draft v1.0 Date: 2026-06-18 Authors: Agent Zero (via grill-with-design on Tabiri infrastructure)


Problem Statement

The Sysloggh agent mesh consists of 6 autonomous agents across 3 distinct frameworks (Hermes Python, Agent Zero, PI/openclaw TypeScript) deployed on 5 Proxmox CTs. These agents operate in isolation — there is no unified inter-agent communication channel. Each agent processes tasks independently, and cross-agent coordination requires manual intervention.

The existing Zulip infrastructure (Zulip 12.0-1 on CT 117, Docker-based) provides a mature async messaging platform, but no agent is wired into it. A partial implementation (zulip-plugin/adapter.py) exists with event streaming and dedup logic, but was never deployed. The gateway adapter is compiled but not running on any agent CT, and the required Python packages are not installed.

The gap: Agents cannot collaboratively route tasks, share context, or coordinate via Zulip threads.


Solution

A multi-platform Zulip agent communication layer consisting of one plugin per agent framework, each running co-located on its agent's CT, connecting its agent to the shared Zulip messaging infrastructure at chat.sysloggh.net.

Architecture at a Glance

┌──────────────────────────────────────────────────────────────────┐
│                      Zulip Server (CT 117)                       │
│                      chat.sysloggh.net:443                        │
│  Stream: #agent-hub                                               │
│  Bot Users: tanko-bot, mumuni-bot, koonimo-bot, koby-bot,        │
│             kagentz-bot, abiba-bot, all-bots                      │
└────┬──────┬──────┬──────┬──────┬──────┬──────┬──────────────────┘
     │      │      │      │      │      │      │
┌────┴┐ ┌───┴──┐ ┌┴────┐ ┌┴────┐ ┌┴────┐ ┌┴────┐ ┌──────────┐
│Tnako│ │Mumuni│ │Kooni│ │Koby │ │Kage │ │Abiba│ │@all-bots │
│Herm │ │Herm  │ │Herm │ │Herm │ │A0   │ │PI   │ │ (special)│
│CT112│ │CT... │ │CT.. │ │CT.. │ │CT.. │ │CT.. │ │           │
└─────┘ └──────┘ └─────┘ └─────┘ └─────┘ └─────┘ └──────────┘
     │      │      │      │      │      │
┌────┴┐ ┌───┴──┐ ┌┴────┐ ┌┴────┐ ┌┴────┐ ┌┴────┐
│Config│ │Config│ │Config│ │Config│ │Config│ │Config│
│tanko │ │mumuni│ │kooni │ │koby  │ │kage  │ │abiba │
│.yaml │ │.yaml │ │.yaml │ │.yaml │ │.yaml │ │.yaml │
└─────┘ └──────┘ └─────┘ └─────┘ └─────┘ └─────┘

Core Principles

  • Co-located: Each plugin runs on the same CT as its agent, uses platform-native interfaces
  • @mention-routed: Messages reach the correct agent via Zulip @mention detection
  • Owner-gated: Private topics are restricted per-agent owner via config
  • Version-controlled: All plugin code lives in a Gitea monorepo, pushed to CTs via deploy script
  • Graceful degradation: Users always receive feedback, even when an agent is unavailable

User Stories

Agent Communication

  1. As a human user, I want to @mention an agent in any Zulip topic, so that the agent receives my message and responds in-thread.

  2. As a human user, I want to @mention @all-bots, so that all 6 agents across all frameworks receive the message.

  3. As an agent, I want to receive messages addressed to me, so that I can process and respond to user requests.

Private Channels

  1. As a human user, I want a private topic per agent (e.g., #agent-hub > tanko-private), so that I can have confidential conversations with a single agent.

  2. As a system administrator, I want per-agent owner configuration, so that only designated users can access each agent's private topic.

Deployment & Operations

  1. As a system administrator, I want to version-control all plugin code in a single Gitea repository, so that updates are tracked, auditable, and pushable across all CTs.

  2. As a system administrator, I want a deploy script that pulls the latest plugin code and restarts services on all CTs, so that updates are consistent and repeatable.

Reliability

  1. As a human user, I want to see a friendly error message if an agent is unavailable, rather than a silent timeout.

  2. As a system administrator, I want health check endpoints on each plugin, so that I can monitor plugin status from the harness or monitoring infra.


Implementation Decisions

Platform Plugin Contracts

Each of the 3 frameworks uses its native plugin system — not a unified bridge or A2A overlay for intra-agent communication. Cross-framework @all-bots messages flow through Zulip: all 7 bot users are subscribed to the same #agent-hub stream, so every bot receives every message. Each bot checks if it's @mentioned, then either processes or ignores.

Framework-Specific Implementation

Framework Plugin Type Interface Reference Architecture
Hermes Python (4 agents) Hermes Gateway BasePlatformAdapter on_event(message) → Response Existing zulip-plugin/ in homelab
PI/openclaw TypeScript (1 agent) openclaw Extension Extension API (similar to existing Discord/GChat/Mattermost/Matrix) /a0/usr/projects/homelab/openclaw/extensions/
Agent Zero (1 agent) A0 Plugin Agent Zero plugin system Custom A0 plugin pattern

@mention Detection

  • Use Zulip SDK's mentioned_users field on message events (ADR-005)
  • Each bot checks if its own user_id is in the mentioned_users array
  • For @all-bots: bot checks if dedicated All Bots user is in mentioned_users (ADR-006)
  • @all-bots spans all 7 bots across all frameworks (ADR-012)

Config Schema (per-agent config.yaml)

# config.yaml (one per bot, stored alongside plugin)
agent:
  name: "Tanko"
  bot_username: "tanko-bot"
  bot_email: "tanko-bot@chat.sysloggh.net"
  bot_api_key: "<api_key>"
  owner_email: "jerome@sysloggh.net"
  private_topic: "tanko-private"

zulip:
  server_url: "https://chat.sysloggh.net"
  stream: "agent-hub"

error_handling:
  timeout_seconds: 30
  retry_count: 3
  graceful_message: "{{agent_name}} is processing your request — please wait..."

monitoring:
  health_endpoint_enabled: true
  health_port: 9200

Private Topic ACL

  • Bot subscribes to its private topic (tanko-private) at startup
  • Owner is matched via config.yaml > owner_email (ADR-011)
  • Messages in private topic: only respond if sender email matches owner
  • Messages in public topics: respond to @mentions from any sender

Error Handling Strategy

  • Agent timeout: plugin sends graceful degradation message to Zulip thread (ADR-009)
  • Zulip server down: SDK auto-reconnects with exponential backoff
  • Invalid message format: plugin logs error, does not crash
  • Health endpoint (/health on port 9200): returns status, uptime, last_message_time

Naming Convention

  • Bot users: {name}-bot (lowercase, dash separator) — tanko-bot, mumuni-bot, etc. (ADR-003)
  • Private topics: {name}-privatetanko-private, mumuni-private, etc. (ADR-001)
  • Display names: capitalize properly — "Tanko Bot", "Mumuni Bot", "All Bots"

Testing Decisions

Unit Tests

  • @mention detection logic: correct extraction of bot from mentioned_users
  • Private topic ACL: owner matches, non-owner denied
  • Config parsing: all fields present, graceful defaults
  • Error response: timeout triggers correct grace message

Integration Tests

  • Each plugin connects to Zulip and receives/sends test messages
  • @all-bots message reaches all 7 bots
  • Private topic message only reaches owner agent
  • Health endpoint returns 200 with valid JSON

Deployment Tests

  • deploy.sh successfully pulls latest and restarts services on all CTs
  • Plugin survives container restart (Zulip reconnection)
  • Config YAML validation before deployment

Out of Scope (v1.0)

  • Multi-realm Zulip: All agents operate within a single Zulip realm
  • Message attachments: v1 sends and receives text only (no file uploads)
  • Rich formatting: v1 uses plain markdown responses from agents
  • A2A as primary transport: Cross-platform communication routes through Zulip, not direct A2A
  • CI/CD pipeline: Deployment is script-based (deploy.sh), not CI/CD
  • Rate limiting: Zulip's built-in rate limiting is sufficient for initial use
  • Dashboard/monitoring UI: Health endpoint data consumed by existing harness infra
  • Message history search: Zulip's native search handles this

Further Notes

Design Decisions Summary

# Decision Rationale
ADR-001 Topic-per-agent with -private suffix Clear naming, easy ACL enforcement
ADR-002 @mention-based routing (not topic-based) User confirmed, flexible for any topic
ADR-003 Bot naming: {name}-bot Consistent, no space issues in Zulip API
ADR-004 One bot per agent, co-located on CT Isolated failures, platform-native
ADR-005 SDK mentioned_users field Zulip API native, always accurate
ADR-006 Dedicated All Bots user Explicit @all-bots, not noisy @everyone
ADR-007 Platform-native plugin contracts Reliability, platform handles state/retries
ADR-008 Native message formats per platform No translation layer needed
ADR-009 Graceful degradation + user-visible errors No silent failures
ADR-010 Gitea monorepo + deploy.sh Single source of truth, versioned
ADR-011 Owner in per-agent config.yaml Explicit, version-controllable
ADR-012 @all-bots spans all frameworks Single @all-bots reaches every agent

Prerequisites

  1. Zulip server operational at https://chat.sysloggh.net
  2. Zulip bot users created (7 total: tanko-bot, mumuni-bot, koonimo-bot, koby-bot, kagentz-bot, abiba-bot, all-bots)
  3. #agent-hub stream created and all bots subscribed
  4. Gitea repo zulip-platform-plugins created and monorepo pushed
  5. Hermes gateway framework verified on Tanko/Mumuni/Koonimo/Koby CTs
  6. openclaw extension API verified on Abiba CT
  7. Agent Zero plugin system verified on Kagentz CT

Deployment Workflow

1. Developer pushes plugin changes to Gitea repo
2. Run: ./scripts/deploy.sh
3. deploy.sh SSHes into each CT:
   a. git pull
   b. Install/update dependencies
   c. Restart plugin service (systemd or supervisor)
4. Verify health endpoint on each CT

Risk Mitigation

  • Template bug in Zulip 12.0-1: Portico-header renders 500 on configs without REGISTRATION_OPEN or with Authentik SSO — resolved via template patch {% if settings.REGISTRATION_OPEN %} guard
  • Docker container restarts: Plugin auto-reconnects via Zulip SDK's built-in reconnect
  • API key rotation: Keys stored in plugin config, updated via Gitea push