diff --git a/README.md b/README.md index b480fc7..6ff171d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,41 @@ -# skill-openclaw-channel +# openclaw-channel plugin -Hermes plugin: send_to_agent tool for inter-agent relay messaging \ No newline at end of file +**Critical infrastructure.** This plugin provides the `send_to_agent` tool — the only way agents can send messages to each other through the relay. + +## What it does + +Registers a single tool (`send_to_agent`) with Hermes' tool registry. The tool POSTs messages to the relay on www0, which routes them to the target agent's webhook. + +## Why it matters + +Without this plugin, agents cannot communicate with each other. Inter-agent coordination, delegation, and status reporting all depend on it. + +**Do not remove this plugin.** It was accidentally deleted once (2026-05-09) and broke all inter-agent messaging until restored. + +## Files + +| File | Purpose | +|------|---------| +| `__init__.py` | Plugin entrypoint — registers the tool with Hermes | +| `send_to_agent.py` | Tool implementation — schema, handler, auth | +| `plugin.yaml` | Plugin metadata | +| `README.md` | This file | + +## Environment variables + +| Variable | Description | Example | +|----------|-------------|---------| +| `OPENCLAW_AGENT_ID` | This agent's name | `bastion` | +| `OPENCLAW_RELAY_URL` | Relay ingress URL | `http://www0:4100` | +| `OPENCLAW_RELAY_SECRET` | Auth secret for relay | (set in container .env) | + +## Message types + +- `fyi` — informational, no reply expected (default) +- `request` — recipient should reply +- `ack` — acknowledging a prior message; terminal + +## Relay docs + +Full architecture: `/opt/data/docs/hermes-comms-bridge.md` +Relay skill: `hermes-relay` diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e3a519a --- /dev/null +++ b/__init__.py @@ -0,0 +1,26 @@ +"""openclaw-channel plugin. + +Registers the send_to_agent tool for inter-agent messaging via openclaw-relay. + +See send_to_agent.py for tool implementation and README.md for context. +""" +from __future__ import annotations + +import logging + +logger = logging.getLogger(__name__) + + +def register(ctx): + from .send_to_agent import _send, _check, SEND_SCHEMA, AGENT_ID + + ctx.register_tool( + name="send_to_agent", + toolset="openclaw", + schema=SEND_SCHEMA, + handler=_send, + check_fn=_check, + description="Send a message to another fleet agent via openclaw-relay.", + emoji="📡", + ) + logger.info("openclaw-channel: registered send_to_agent (agent=%s)", AGENT_ID) diff --git a/plugin.yaml b/plugin.yaml new file mode 100644 index 0000000..6cd2e74 --- /dev/null +++ b/plugin.yaml @@ -0,0 +1,5 @@ +name: openclaw-channel +version: 0.1.0 +description: "openclaw-relay inter-agent channel — adds send_to_agent tool for fleet messaging" +provides_tools: + - send_to_agent diff --git a/send_to_agent.py b/send_to_agent.py new file mode 100644 index 0000000..50a482b --- /dev/null +++ b/send_to_agent.py @@ -0,0 +1,131 @@ +"""send_to_agent — inter-agent messaging via openclaw-relay. + +This tool is the OUTBOUND half of the relay channel. It lets agents send +messages to other fleet agents through the relay on www0. + +INBOUND messages arrive via the webhook platform (config.yaml → +platforms.webhook.routes.openclaw), delivered by the relay to each +agent's webhook endpoint. + +CRITICAL: This tool is how agents talk to each other. Without it, +inter-agent coordination is dead. Do NOT remove or rename without +replacing the functionality. + +Environment variables required: + OPENCLAW_AGENT_ID — this agent's name (e.g. "bastion", "vigil") + OPENCLAW_RELAY_URL — relay ingress URL (e.g. http://www0:4100) + OPENCLAW_RELAY_SECRET — auth secret for relay ingress +""" +from __future__ import annotations + +import json +import logging +import os +from typing import Any +from urllib import error as urlerror, request as urlrequest + +from tools.registry import tool_error + +logger = logging.getLogger(__name__) + +AGENT_ID = os.environ.get("OPENCLAW_AGENT_ID", "bastion") +RELAY_URL = os.environ.get("OPENCLAW_RELAY_URL", "http://www0:4100").rstrip("/") +SECRET = (os.environ.get("OPENCLAW_RELAY_SECRET") or "").strip() + +KNOWN_AGENTS = [ + "atlas", "bastion", "claude_memgpt", "clio", "ferret", "flux", + "mint", "nene", "piper", "reed", "scout", "skippy", "tangent", + "terra", "vera", "vigil", +] + +SEND_SCHEMA = { + "name": "send_to_agent", + "description": ( + f"Send a message to another fleet agent via openclaw-relay as " + f"{AGENT_ID}.\n\n" + "Types:\n" + "- fyi: informational, no reply expected (default)\n" + "- request: the recipient should reply\n" + "- ack: acknowledging a prior message; terminal\n\n" + f"Known agents: {', '.join(KNOWN_AGENTS)}.\n" + "Returns the relay's JSON response (delivered/queued/error)." + ), + "parameters": { + "type": "object", + "properties": { + "to": { + "type": "string", + "description": "Target agent name (lowercase). Example: terra, claude_memgpt.", + }, + "message": { + "type": "string", + "description": "Message body to send.", + }, + "type": { + "type": "string", + "enum": ["fyi", "request", "ack"], + "default": "fyi", + "description": "Message classification.", + }, + "project": { + "type": "string", + "description": "Optional project tag for relay-side grouping/filtering.", + }, + "inReplyTo": { + "type": "string", + "description": "Optional messageId being replied to.", + }, + }, + "required": ["to", "message"], + }, +} + + +def _send(args: dict, **kwargs: Any) -> str: + to = str(args.get("to") or "").lower().strip() + message = args.get("message") + if not to or not message: + return tool_error("to and message are required") + if not SECRET: + return tool_error( + "OPENCLAW_RELAY_SECRET not set; cannot authenticate to openclaw-relay" + ) + + body: dict[str, Any] = { + "from": AGENT_ID, + "to": to, + "type": args.get("type") or "fyi", + "body": message, + } + if args.get("project"): + body["project"] = args["project"] + if args.get("inReplyTo"): + body["inReplyTo"] = args["inReplyTo"] + + raw = json.dumps(body).encode("utf-8") + req = urlrequest.Request( + f"{RELAY_URL}/msg", + data=raw, + method="POST", + headers={ + "content-type": "application/json", + "x-relay-agent": AGENT_ID, + "x-relay-secret": SECRET, + }, + ) + try: + with urlrequest.urlopen(req, timeout=15) as resp: + return resp.read().decode("utf-8", errors="replace") + except urlerror.HTTPError as e: + detail = "" + try: + detail = e.read().decode("utf-8", errors="replace")[:500] + except Exception: + pass + return tool_error(f"relay HTTP {e.code}", detail=detail) + except Exception as e: + return tool_error(f"relay request failed: {e}") + + +def _check() -> bool: + return bool(SECRET)