From 66c6d4966e635b069896ad9aa909c61199f801b5 Mon Sep 17 00:00:00 2001 From: "Abiba (pi)" Date: Fri, 19 Jun 2026 23:55:19 +0000 Subject: [PATCH] fix: correct Abiba platform label (pi not openclaw), add plugin skeletons, shared config, fix health port - Replace all 'openclaw' references with 'pi' (pi coding harness) - Fix Git remote URL in CONTEXT.md - Harmonize health port to 9200 across all docs - Add config.yaml.example with full schema - Add pi-zulip-extension skeleton (TypeScript, pi Extension API) - Add hermes-zulip-plugin skeleton (Python, BasePlatformAdapter) - Add agent-zero-plugin skeleton (Python, A0 plugin API) - Update ADR-007 with pi extension docs reference - Document private topic ACL v1 limitation in CONTEXT.md --- agent-zero-plugin/requirements.txt | 2 + agent-zero-plugin/src/__init__.py | 10 ++ agent-zero-plugin/src/plugin.py | 21 +++ config.yaml.example | 30 +++++ docs/ARCHITECTURE.md | 84 ++++++------ docs/CONTEXT.md | 22 ++-- docs/adr/ADR-007-platform-native-contracts.md | 11 +- hermes-zulip-plugin/requirements.txt | 3 + .../src/hermes_zulip/__init__.py | 11 ++ .../src/hermes_zulip/adapter.py | 26 ++++ pi-zulip-extension/package.json | 22 ++++ pi-zulip-extension/src/index.ts | 120 ++++++++++++++++++ pi-zulip-extension/tsconfig.json | 14 ++ 13 files changed, 320 insertions(+), 56 deletions(-) create mode 100644 agent-zero-plugin/requirements.txt create mode 100644 agent-zero-plugin/src/__init__.py create mode 100644 agent-zero-plugin/src/plugin.py create mode 100644 config.yaml.example create mode 100644 hermes-zulip-plugin/requirements.txt create mode 100644 hermes-zulip-plugin/src/hermes_zulip/__init__.py create mode 100644 hermes-zulip-plugin/src/hermes_zulip/adapter.py create mode 100644 pi-zulip-extension/package.json create mode 100644 pi-zulip-extension/src/index.ts create mode 100644 pi-zulip-extension/tsconfig.json diff --git a/agent-zero-plugin/requirements.txt b/agent-zero-plugin/requirements.txt new file mode 100644 index 0000000..4bac78f --- /dev/null +++ b/agent-zero-plugin/requirements.txt @@ -0,0 +1,2 @@ +zulip>=0.9.0 +pyyaml>=6.0 diff --git a/agent-zero-plugin/src/__init__.py b/agent-zero-plugin/src/__init__.py new file mode 100644 index 0000000..3fde7ae --- /dev/null +++ b/agent-zero-plugin/src/__init__.py @@ -0,0 +1,10 @@ +""" +Agent Zero Zulip Plugin — Core plugin for Agent Zero + +Connects kagentz (Agent Zero on CT 105) to the Sysloggh Zulip agent mesh +via the Agent Zero plugin API. + +Config: config.yaml (per-agent, deployed alongside plugin) +""" + +__version__ = "0.1.0" diff --git a/agent-zero-plugin/src/plugin.py b/agent-zero-plugin/src/plugin.py new file mode 100644 index 0000000..1c4a662 --- /dev/null +++ b/agent-zero-plugin/src/plugin.py @@ -0,0 +1,21 @@ +# Agent Zero Zulip Plugin — Core implementation +# See ADR-005 for @mention detection, ADR-009 for error handling +# Full implementation in issue #13. This skeleton establishes the package. + + +class AgentZeroZulipPlugin: + """Zulip plugin for Agent Zero. Connects kagentz to #agent-hub, + detects @mentions, routes messages to/from the Agent Zero agent.""" + + def __init__(self, config: dict) -> None: + self.config = config + self.connected = False + + async def connect(self) -> None: + raise NotImplementedError("Skeleton — implement in issue #13") + + async def disconnect(self) -> None: + raise NotImplementedError("Skeleton — implement in issue #13") + + async def handle_message(self, event: dict) -> dict | None: + raise NotImplementedError("Skeleton — implement in issue #13") diff --git a/config.yaml.example b/config.yaml.example new file mode 100644 index 0000000..9dc15c4 --- /dev/null +++ b/config.yaml.example @@ -0,0 +1,30 @@ +# config.yaml.example — Shared configuration schema for all 3 platform plugins +# Copy to config.yaml and fill in per-agent values. Do NOT commit config.yaml. +# +# Fields marked ${ENV_VAR} should be set from environment variables. +# All fields are required unless marked (optional). + +agent: + name: "tanko" # Agent name (lowercase, no spaces) + display_name: "Tanko" # Human-readable display name + zulip_bot_name: "tanko-bot" # Zulip bot username (ADR-003 naming) + owner_email: "jerome@sysloggh.net" # Human owner for private topic ACL + private_topic: "tanko-private" # Private topic name (ADR-001 naming) + +zulip: + server_url: "https://chat.sysloggh.net" + email: "tanko-bot@chat.sysloggh.net" + api_key: "${ZULIP_API_KEY}" # Zulip bot API key — use env var, never hardcode + stream: "agent-hub" # Zulip stream for agent communication + all_bots_user_id: 1234 # User ID of the "All Bots" bot (ADR-006) + +error_handling: + timeout_seconds: 30 # Max wait for agent response + retry_count: 3 # Retry attempts before graceful degradation + retry_delay_seconds: 5 # Delay between retries + graceful_message: "{{agent_name}} is processing your request — please wait..." + +monitoring: + health_endpoint_enabled: true + health_port: 9200 # Health check HTTP port (all platforms) + log_level: "INFO" # DEBUG, INFO, WARNING, ERROR diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 2eb94e0..31739b5 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,20 +1,18 @@ # Zulip Multi-Platform Agent Communication — Architecture ## Overview - -A production-ready system enabling 6 AI agents across 3 platforms (Hermes Python, Agent Zero, PI/openclaw TypeScript) to communicate through Zulip via dedicated per-agent bot users. +A production-ready system enabling 6 AI agents across 3 platforms (Hermes Python, Agent Zero, pi TypeScript) to communicate through Zulip via dedicated per-agent bot users. ## Architecture Diagram - ``` ┌─────────────────────────────────────────────────────────────┐ -│ Zulip Server (CT 117) │ -│ chat.sysloggh.net:443 │ +│ Zulip Server (CT 117) │ +│ chat.sysloggh.net:443 │ │ │ -│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─── │ -│ │tanko-bot │ │mumuni-bot│ │koonimo │ │koby-bot │ │... │ -│ │ │ │ │ │-bot │ │ │ │ │ -│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ └─── │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─── │ +│ │tanko-bot │ │mumuni-bot│ │koonimo │ │koby-bot │ │... │ +│ │ │ │ │ │-bot │ │ │ │ │ +│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ └─── │ └───────┼────────────┼────────────┼────────────┼──────────────┘ │ │ │ │ ┌────▼────┐ ┌────▼────┐ ┌────▼────┐ ┌────▼────┐ @@ -26,41 +24,39 @@ A production-ready system enabling 6 AI agents across 3 platforms (Hermes Python │ CT 112 │ │ CT 114 │ │ CT 113 │ │ CT 111 │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ - ┌──────────────┐ ┌──────────────┐ - │ Agent Zero │ │ PI/openclaw │ - │ Plugin │ │ Extension │ - │ (Python) │ │ (TypeScript) │ - │ │ │ │ - │ kagentz │ │ abiba │ - │ CT 105 │ │ CT 100 │ - └──────────────┘ └──────────────┘ + ┌──────────────┐ ┌──────────────┐ + │ Agent Zero │ │ pi │ + │ Plugin │ │ Extension │ + │ (Python) │ │ (TypeScript) │ + │ │ │ │ + │ kagentz │ │ abiba │ + │ CT 105 │ │ CT 100 │ + └──────────────┘ └──────────────┘ ``` ## Message Flow (Normal) - ``` User creates topic "project-x" in #agent-hub └── User types: @tanko-bot analyze this data - └── Zulip fires event to tanko-bot's event queue - └── Hermes Zulip Plugin on CT 112 - ├── Detects: tanko-bot in mentioned_users - ├── Strips @mention from message - ├── Constructs MessageEvent - └── Routes to Tanko agent via BasePlatformAdapter - └── Tanko agent processes, returns response - └── Plugin posts to #agent-hub > project-x + └── Zulip fires event to tanko-bot's event queue + └── Hermes Zulip Plugin on CT 112 + ├── Detects: tanko-bot in mentioned_users + ├── Strips @mention from message + ├── Constructs MessageEvent + └── Routes to Tanko agent via BasePlatformAdapter + └── Tanko agent processes, returns response + └── Plugin posts to #agent-hub > project-x ``` ## Message Flow (@all-bots) - ``` User types: @all-bots status report └── Zulip fires event to ALL 6 bots (same narrow) - └── Each plugin independently: - ├── Detects: All Bots in mentioned_users - ├── Forwards to its agent - └── Posts response to same topic - └── 6 independent responses appear in the topic + └── Each plugin independently: + ├── Detects: All Bots in mentioned_users + ├── Forwards to its agent + └── Posts response to same topic + └── 6 independent responses appear in the topic ``` ## Platform Plugin Contracts @@ -76,21 +72,20 @@ User types: @all-bots status report - Implements: Agent Zero plugin API - Config: `config.yaml` per-agent -### PI/openclaw (TypeScript) — openclaw Extension API -- Path: `openclaw-zulip-extension/src/` -- Implements: openclaw Extension +### pi (TypeScript) — pi Extension API +- Path: `pi-zulip-extension/src/` +- Implements: pi extension (TypeScript module in `~/.pi/agent/extensions/`) - Config: `config.yaml` per-agent +- Reference: pi extension docs at `/usr/lib/node_modules/@earendil-works/pi-coding-agent/docs/extensions.md` ## Reliability - - **Reconnection**: Zulip SDK handles backoff/reconnect natively - **Retry**: 3 attempts with 5s backoff on agent timeout - **Error Response**: User-visible message on failure -- **Monitoring**: Health endpoint (`/health`) on each plugin +- **Monitoring**: Health endpoint (`/health`) on each plugin (port 9200) - **Logging**: Per-plugin log files ## Config Template - ```yaml # config.yaml — Per-agent Zulip plugin configuration agent: @@ -106,16 +101,19 @@ zulip: api_key: "${ZULIP_API_KEY}" # From env var all_bots_user_id: 1234 -agent: - max_retries: 3 +error_handling: + timeout_seconds: 30 + retry_count: 3 retry_delay_seconds: 5 - health_port: 8080 + +monitoring: + health_endpoint_enabled: true + health_port: 9200 ``` ## Prerequisites - 1. Zulip server running on CT 117 2. Zulip bot users created for all 6 agents + All Bots 3. `#agent-hub` stream created with all bots subscribed 4. SSH key access to all 6 agent CTs -5. Gitea repo initialized +5. Gitea repo initialized at `git@git.sysloggh.net:SyslogSolution/zulip-platform-plugins.git` diff --git a/docs/CONTEXT.md b/docs/CONTEXT.md index 6048007..9575309 100644 --- a/docs/CONTEXT.md +++ b/docs/CONTEXT.md @@ -6,7 +6,7 @@ This document captures the shared language and architectural invariants for the | Term | Definition | |------|------------| -| **Agent** | An AI agent operating on a platform (Hermes/Agent Zero/PI/openclaw) that can receive and respond to Zulip messages | +| **Agent** | An AI agent operating on a platform (Hermes/Agent Zero/pi) that can receive and respond to Zulip messages | | **Adapter / Plugin** | A platform-native component that bridges Zulip events to the agent framework | | **@mention** | A Zulip mention (`@**Tanko Bot**`) that triggers the named agent to respond | | **@all-bots** | A dedicated Zulip bot user that, when @mentioned, triggers ALL agents across all platforms | @@ -15,10 +15,9 @@ This document captures the shared language and architectural invariants for the | **Owner** | The human user associated with an agent bot, stored in per-agent config | ## Naming Conventions - - Agent Zulip bot display names: lowercase, dash-separated, `-bot` suffix — e.g., `tanko-bot`, `mumuni-bot` -- Agent Zulip bot emails: `@chat.sysloggh.net` — e.g., `tanko-bot@chat.sysloggh.net` -- Private topics: `-private` — e.g., `tanko-private` +- Agent Zulip bot emails: `@chat.sysloggh.net` — e.g., `tanko-bot@chat.sysloggh.net` +- Private topics: `{name}-private` — e.g., `tanko-private` - Owner privacy: agent should never expose owner name to other agents or users ## Agent Registry @@ -30,30 +29,31 @@ This document captures the shared language and architectural invariants for the | koonimo | Hermes (Python) | koonimo-bot | amdpve CT 113 | koonimo-private | | koby | Hermes (Python) | koby-bot | amdpve CT 111 | koby-private | | kagentz | Agent Zero | kagentz-bot | amdpve CT 105 | kagentz-private | -| abiba | PI/openclaw (TypeScript) | abiba-bot | amdpve CT 100 | abiba-private | +| abiba | pi (TypeScript) | abiba-bot | amdpve CT 100 | abiba-private | ## @all-bots Resolution -@all-bots refers to all 6 agents across all frameworks (Hermes, Agent Zero, PI/openclaw). When @all-bots is mentioned in any topic: - +@all-bots refers to all 6 agents across all frameworks (Hermes, Agent Zero, pi). When @all-bots is mentioned in any topic: 1. Each per-agent bot receives the event (since they all share `narrow: stream agent-hub`) 2. Each bot checks: "Is `All Bots` in `mentioned_users`?" 3. If yes, bot forwards the message to its agent 4. @all-bots is a **dedicated Zulip bot user** with display name `All Bots` and email `all-bots@chat.sysloggh.net` ## Response Rules - - **Own topic**: Agent always responds (to its private topic) - **Collaboration topic** (any topic that isn't a private topic): Agent responds only when @mentioned - **@all-bots**: All agents respond -- **Private topics**: Only the agent owner + bot are participants; bot enforces this +- **Private topics**: Only the agent owner + bot are participants; bot enforces this via config owner_email matching + +## Private Topic ACL (v1 — documented limitation) + +In v1, private topic access is enforced by matching `sender_email == config.owner_email` on each message. **This is not cryptographically authenticated** — Zulip does not enforce sender identity. A user who knows the owner email can impersonate them. This is an acceptable limitation for v1 internal use. v2 should use Zulip invite-only stream permissions or shared-secret challenge-response. ## Monorepo Structure -All 3 platform plugins live in a single Gitea repository at `git@192.168.68.17:homelab/zulip-platform-plugins.git` +All 3 platform plugins live in a single Gitea repository at `git@git.sysloggh.net:SyslogSolution/zulip-platform-plugins.git` ## Guardian Directives - - All destructive actions require user confirmation before execution - Plugin updates are version-controlled through Gitea - Config files are per-agent and pushed alongside code diff --git a/docs/adr/ADR-007-platform-native-contracts.md b/docs/adr/ADR-007-platform-native-contracts.md index 9f680b7..1a426b1 100644 --- a/docs/adr/ADR-007-platform-native-contracts.md +++ b/docs/adr/ADR-007-platform-native-contracts.md @@ -3,13 +3,20 @@ **Date**: 2026-04-28 ### Decision -Each platform uses its native plugin interface: Hermes `BasePlatformAdapter`, openclaw extension API, Agent Zero plugin system. + +Each platform uses its native plugin interface: Hermes `BasePlatformAdapter`, pi extension API, Agent Zero plugin system. ### Context + Each platform has matured, battle-tested interfaces for receiving and sending messages. A unified A2A contract would add unnecessary abstraction and latency for local agent communication. Platform-native contracts are reliable, handle connection-state/retry automatically, and follow each platform's established patterns. +- **Hermes**: `BasePlatformAdapter` — Python, gateway-based, handles connection lifecycle +- **pi**: Extension API — TypeScript modules in `~/.pi/agent/extensions/`, registers tools via `pi.registerTool()`, hooks lifecycle events. See pi docs at `/usr/lib/node_modules/@earendil-works/pi-coding-agent/docs/extensions.md` +- **Agent Zero**: A0 plugin system — Python, agent-native plugin API + ### Consequences + - Three separate plugin implementations - Each plugin leverages platform-specific reliability features - Plugin developers need familiarity with each platform -- Cross-platform @all-bots still works because each bot processes the same Zulip event independently \ No newline at end of file +- Cross-platform @all-bots still works because each bot processes the same Zulip event independently diff --git a/hermes-zulip-plugin/requirements.txt b/hermes-zulip-plugin/requirements.txt new file mode 100644 index 0000000..e6a2a4f --- /dev/null +++ b/hermes-zulip-plugin/requirements.txt @@ -0,0 +1,3 @@ +zulip>=0.9.0 +zulip-bots>=0.9.0 +pyyaml>=6.0 diff --git a/hermes-zulip-plugin/src/hermes_zulip/__init__.py b/hermes-zulip-plugin/src/hermes_zulip/__init__.py new file mode 100644 index 0000000..a593145 --- /dev/null +++ b/hermes-zulip-plugin/src/hermes_zulip/__init__.py @@ -0,0 +1,11 @@ +""" +Hermes Zulip Plugin — Core adapter for Hermes Python agents + +Implements BasePlatformAdapter to connect Hermes agents (Tanko, Mumuni, +Koonimo, Koby) to the Sysloggh Zulip agent mesh. + +Path: hermes-zulip-plugin/src/hermes_zulip/ +Config: config.yaml (per-agent, deployed alongside plugin) +""" + +__version__ = "0.1.0" diff --git a/hermes-zulip-plugin/src/hermes_zulip/adapter.py b/hermes-zulip-plugin/src/hermes_zulip/adapter.py new file mode 100644 index 0000000..e9ee789 --- /dev/null +++ b/hermes-zulip-plugin/src/hermes_zulip/adapter.py @@ -0,0 +1,26 @@ +# Core adapter — implements BasePlatformAdapter for Zulip +# See ADR-005 for @mention detection, ADR-009 for error handling +# Full implementation in issue #5. This skeleton establishes the package. + +from typing import Any, Dict, Optional + + +class ZulipAdapter: + """Zulip adapter for Hermes agents. Connects to Zulip, listens for + @mentions in #agent-hub, routes messages via BasePlatformAdapter.""" + + def __init__(self, config: Dict[str, Any]) -> None: + self.config = config + self.connected = False + + async def connect(self) -> None: + raise NotImplementedError("Skeleton — implement in issue #5") + + async def disconnect(self) -> None: + raise NotImplementedError("Skeleton — implement in issue #5") + + async def send_message(self, topic: str, content: str) -> Dict[str, Any]: + raise NotImplementedError("Skeleton — implement in issue #5") + + async def on_event(self, event: Dict[str, Any]) -> Optional[Dict[str, Any]]: + raise NotImplementedError("Skeleton — implement in issue #5") diff --git a/pi-zulip-extension/package.json b/pi-zulip-extension/package.json new file mode 100644 index 0000000..cdce1c1 --- /dev/null +++ b/pi-zulip-extension/package.json @@ -0,0 +1,22 @@ +{ + "name": "pi-zulip-extension", + "version": "0.1.0", + "description": "pi extension for Zulip agent communication — connects Abiba to the Sysloggh agent mesh", + "main": "src/index.ts", + "type": "module", + "scripts": { + "build": "tsc", + "check": "tsc --noEmit" + }, + "dependencies": { + "zulip-js": "^2.0.0", + "yaml": "^2.0.0" + }, + "devDependencies": { + "@earendil-works/pi-coding-agent": "*", + "typescript": "^5.0.0" + }, + "keywords": ["pi", "zulip", "agent", "abiba", "sysloggh"], + "license": "UNLICENSED", + "private": true +} diff --git a/pi-zulip-extension/src/index.ts b/pi-zulip-extension/src/index.ts new file mode 100644 index 0000000..992e66a --- /dev/null +++ b/pi-zulip-extension/src/index.ts @@ -0,0 +1,120 @@ +/** + * pi-zulip-extension — Zulip agent communication plugin for pi agents + * + * Deploy to: ~/.pi/agent/extensions/zulip.ts + * Config: config.yaml (alongside extension, or symlinked) + * + * Connects a pi agent (Abiba) to the Sysloggh Zulip agent mesh. + * Listens for @mentions of abiba-bot in #agent-hub and forwards + * to the pi agent. Responses are posted back to the same Zulip topic. + * + * @see ADR-007: Platform-native plugin contracts + * @see docs/ARCHITECTURE.md + */ + +import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; +import { readFileSync } from "fs"; +import { parse } from "yaml"; + +// --------------------------------------------------------------------------- +// Config +// --------------------------------------------------------------------------- + +interface ZulipConfig { + agent: { + name: string; + display_name: string; + zulip_bot_name: string; + owner_email: string; + private_topic: string; + }; + zulip: { + server_url: string; + email: string; + api_key: string; + stream: string; + all_bots_user_id: number; + }; + error_handling: { + timeout_seconds: number; + retry_count: number; + retry_delay_seconds: number; + graceful_message: string; + }; + monitoring: { + health_endpoint_enabled: boolean; + health_port: number; + log_level: string; + }; +} + +function loadConfig(): ZulipConfig { + const raw = readFileSync("config.yaml", "utf-8"); + const cfg = parse(raw) as ZulipConfig; + cfg.zulip.api_key = process.env["ZULIP_API_KEY"] ?? cfg.zulip.api_key; + return cfg; +} + +// --------------------------------------------------------------------------- +// Extension entry point +// --------------------------------------------------------------------------- + +export default function (pi: ExtensionAPI) { + const config = loadConfig(); + + // Health endpoint + if (config.monitoring.health_endpoint_enabled) { + startHealthServer(config); + } + + pi.on("session_start", async (_event, ctx) => { + ctx.ui.notify( + `Zulip extension loaded for ${config.agent.display_name} (${config.agent.zulip_bot_name})`, + "info" + ); + }); + + // Diagnostic tool + pi.registerTool({ + name: "zulip_status", + label: "Zulip Status", + description: "Check Zulip connection status and bot identity", + parameters: {}, + execute: async () => ({ + connected: false, // TODO: track connection state + bot: config.agent.zulip_bot_name, + stream: config.zulip.stream, + server: config.zulip.server_url, + owner: config.agent.owner_email, + private_topic: config.agent.private_topic, + }), + }); +} + +// --------------------------------------------------------------------------- +// Health endpoint +// --------------------------------------------------------------------------- + +function startHealthServer(config: ZulipConfig) { + const http = require("http") as typeof import("http"); + const port = config.monitoring.health_port; + const startTime = Date.now(); + + http + .createServer((_req: any, res: any) => { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + status: "ok", + agent: config.agent.name, + uptime_seconds: Math.floor((Date.now() - startTime) / 1000), + zulip_connected: false, + last_message_time: null, + timestamp: new Date().toISOString(), + }) + ); + }) + .listen(port, "127.0.0.1", () => { + console.log(`[zulip-extension] Health endpoint on :${port}`); + }); +} diff --git a/pi-zulip-extension/tsconfig.json b/pi-zulip-extension/tsconfig.json new file mode 100644 index 0000000..00dbb5d --- /dev/null +++ b/pi-zulip-extension/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "outDir": "dist", + "rootDir": "src", + "declaration": true, + "skipLibCheck": true + }, + "include": ["src/**/*.ts"] +}