contract: Zulip extension verification report + cross-platform contract
- Full contract verification for pi (TypeScript) and Hermes (Python) Zulip plugins - Fixed @all-bots user_id (1→20) — now dynamically resolved from Zulip API - Fixed last_event_id stale display in health endpoint (B2) - Fixed display_recipient logging (B3) - Added bot_user_id and all_bots_user_id to health endpoint - Created OpenProse cross-platform verification contract (docs/contracts/) - Created Hermes plugin deployment script (scripts/deploy-hermes-zulip.py) Pi extension: 8/9 checks pass (LLM relay untested — no external DMs) Hermes adapter: 9/10 checks pass (deployment pending)
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Deploy Hermes Zulip plugin as a systemd service.
|
||||
Creates service file, installs dependencies, deploys plugin.
|
||||
|
||||
Usage: python3 deploy-hermes-zulip.py --agent tanko --email tanko-bot@chat.sysloggh.net --api-key KEY
|
||||
"""
|
||||
import argparse, os, sys, subprocess, json
|
||||
|
||||
SYSTEMD_TEMPLATE = """[Unit]
|
||||
Description=Hermes Zulip Gateway — {agent_name} ({bot_email})
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
Documentation=https://git.sysloggh.net/SyslogSolution/zulip-platform-plugins
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/opt/hermes-zulip-{agent_name}
|
||||
Environment=ZULIP_SITE=https://chat.sysloggh.net
|
||||
Environment=ZULIP_EMAIL={bot_email}
|
||||
Environment=ZULIP_API_KEY={api_key}
|
||||
Environment=ZULIP_AGENT_NAME={agent_name}
|
||||
Environment=ZULIP_STREAM=agent-hub
|
||||
Environment=ZULIP_POLL_INTERVAL=3
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
ExecStart=/usr/bin/python3 -m hermes_zulip.adapter_standalone
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=hermes-zulip-{agent_name}
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
"""
|
||||
|
||||
PLUGIN_FILES = [
|
||||
"plugins/platforms/zulip/__init__.py",
|
||||
"plugins/platforms/zulip/adapter.py",
|
||||
"plugins/platforms/zulip/plugin.yaml",
|
||||
]
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Deploy Hermes Zulip plugin")
|
||||
parser.add_argument("--agent", required=True, help="Agent name (e.g. tanko)")
|
||||
parser.add_argument("--email", required=True, help="Bot email")
|
||||
parser.add_argument("--api-key", required=True, help="Zulip API key")
|
||||
parser.add_argument("--port", type=int, default=None, help="Health port (auto-assigned)")
|
||||
args = parser.parse_args()
|
||||
|
||||
agent = args.agent.lower()
|
||||
port = args.port or (9201 + hash(agent) % 10)
|
||||
repo_dir = "/root/zulip-platform-plugins"
|
||||
deploy_dir = f"/opt/hermes-zulip-{agent}"
|
||||
|
||||
print(f"=== Deploying Hermes Zulip Plugin: {agent} ===")
|
||||
print(f" Bot: {args.email}")
|
||||
print(f" Port: {port}")
|
||||
print(f" Deploy dir: {deploy_dir}")
|
||||
print()
|
||||
|
||||
# 1. Ensure httpx is installed
|
||||
print("[1/5] Installing httpx...")
|
||||
subprocess.run(
|
||||
[sys.executable, "-m", "pip", "install", "--break-system-packages", "--quiet", "httpx"],
|
||||
check=False,
|
||||
)
|
||||
print(" ✅ httpx installed")
|
||||
|
||||
# 2. Create deploy directory
|
||||
print(f"[2/5] Setting up {deploy_dir}...")
|
||||
os.makedirs(deploy_dir, exist_ok=True)
|
||||
|
||||
# 3. Copy plugin files
|
||||
print("[3/5] Copying plugin files...")
|
||||
for f in PLUGIN_FILES:
|
||||
src = os.path.join(repo_dir, f)
|
||||
dst = os.path.join(deploy_dir, os.path.basename(f))
|
||||
if os.path.exists(src):
|
||||
subprocess.run(["cp", src, dst], check=True)
|
||||
print(f" ✅ {os.path.basename(f)}")
|
||||
else:
|
||||
print(f" ⚠️ {f} not found, skipping")
|
||||
|
||||
# 4. Create standalone adapter launcher
|
||||
print("[4/5] Creating adapter launcher...")
|
||||
launcher = os.path.join(deploy_dir, "adapter_standalone.py")
|
||||
with open(launcher, "w") as f:
|
||||
f.write(f'''"""Standalone launcher for Hermes Zulip adapter — {agent}."""
|
||||
import asyncio, os, sys, logging
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
)
|
||||
|
||||
sys.path.insert(0, "{deploy_dir}")
|
||||
|
||||
from adapter import ZulipAdapter, check_requirements
|
||||
|
||||
async def main():
|
||||
if not check_requirements():
|
||||
print("Missing Zulip credentials. Set ZULIP_SITE, ZULIP_EMAIL, ZULIP_API_KEY.")
|
||||
sys.exit(1)
|
||||
|
||||
config = type("Config", (), {{"extra": {{}}}})()
|
||||
adapter = ZulipAdapter(config)
|
||||
|
||||
ok = await adapter.connect()
|
||||
if not ok:
|
||||
print("Failed to connect to Zulip")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Connected as {{adapter._email}} (queue={{adapter._queue_id}})")
|
||||
|
||||
# Keep alive
|
||||
try:
|
||||
await asyncio.Event().wait()
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
finally:
|
||||
await adapter.disconnect()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
''')
|
||||
print(" ✅ adapter_standalone.py")
|
||||
|
||||
# 5. Create and enable systemd service
|
||||
print("[5/5] Installing systemd service...")
|
||||
service_name = f"hermes-zulip-{agent}"
|
||||
service_content = SYSTEMD_TEMPLATE.format(
|
||||
agent_name=agent.title(),
|
||||
bot_email=args.email,
|
||||
api_key=args.api_key,
|
||||
port=port,
|
||||
)
|
||||
|
||||
service_path = f"/etc/systemd/system/{service_name}.service"
|
||||
with open(service_path, "w") as f:
|
||||
f.write(service_content)
|
||||
|
||||
subprocess.run(["systemctl", "daemon-reload"], check=True)
|
||||
subprocess.run(["systemctl", "enable", service_name], check=True)
|
||||
print(f" ✅ Service installed: {service_name}")
|
||||
print(f" Service file: {service_path}")
|
||||
print()
|
||||
print(f" To start: systemctl start {service_name}")
|
||||
print(f" To check: systemctl status {service_name}")
|
||||
print(f" Logs: journalctl -u {service_name} -f")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user