feat: openclaw-channel plugin - send_to_agent tool for inter-agent relay

This commit is contained in:
Bastion 2026-05-17 19:09:39 -07:00
parent 25c02523bf
commit 542571ccc5
4 changed files with 202 additions and 2 deletions

View File

@ -1,3 +1,41 @@
# skill-openclaw-channel # openclaw-channel plugin
Hermes plugin: send_to_agent tool for inter-agent relay messaging **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`

26
__init__.py Normal file
View File

@ -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)

5
plugin.yaml Normal file
View File

@ -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

131
send_to_agent.py Normal file
View File

@ -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)