@m64/nats-channel
v0.4.1
Published
NATS Agent Protocol channel for OpenClaw. Makes every OpenClaw agent a discoverable, spec-compliant agent on NATS.
Maintainers
Readme
@synadia/nats-channel
Currently published on npm as
@m64/nats-channel; moving to@synadia/nats-channelonce Synadia publishing access lands. Install commands below use the current name.
NATS channel plugin for OpenClaw, implementing the NATS Agent Protocol v0.2.0.
Every configured OpenClaw agent becomes a discoverable, addressable, streaming agent on NATS. Callers using any SDK that speaks the protocol - e.g. @synadia/agents - can enumerate running OpenClaw agents, prompt them, and stream responses back.
Sibling implementations sharing the same wire protocol: pi (PI), claude-code (Claude Code).
What each agent exposes
When OpenClaw starts the channel:
- Connects to NATS using the configured URL and optional credentials.
- Registers a NATS micro service named
agentswith spec metadata (agent,owner,session,protocol_version). - Adds a
promptendpoint atagents.oc.<owner>.<agentName>advertisingmax_payload: 1MBandattachments_ok: true. - Publishes heartbeats on
agents.oc.<owner>.<agentName>.heartbeatevery 30 s. - On each inbound prompt: decodes any attached files to
~/.openclaw/attachments/<agentName>/<uuid>/<filename>, prepends their absolute paths to the prompt text, emits astatus: ackchunk, dispatches the augmented prompt into OpenClaw's direct-DM pipeline, and streams each delivered block back as a typed{type:"response",data}chunk, terminating with the spec-mandated empty-body no-headers terminator. - Agent-initiated messages (the old
sendTextoutbound path) still publish toagents.oc.<owner>.<agentName>.outbound- an OpenClaw-specific extension, not part of the spec.
Malformed envelopes, oversized payloads, invalid base64, and unsafe filenames are rejected at the wire with Nats-Service-Error-Code: 400. Staging and dispatch failures return 500.
Install
openclaw plugins install @m64/nats-channelOr use the one-line installer with a guided config wizard:
curl -fsSL https://m64.io/nats-channels/openclaw.sh | bashConfigure
Run the built-in setup wizard:
openclaw configure --section channelsSelect NATS Agent Network and follow the prompts.
Or set fields via CLI:
openclaw config set channels.nats.accounts.default.agentName "my-agent"
openclaw config set channels.nats.accounts.default.url "nats://demo.nats.io"
openclaw config set channels.nats.accounts.default.description "My agent"
openclaw config set channels.nats.accounts.default.owner "acme"Or write to ~/.openclaw/openclaw.json:
{
"channels": {
"nats": {
"accounts": {
"default": {
"url": "nats://demo.nats.io",
"agentName": "my-agent",
"description": "My OpenClaw agent",
"owner": "acme"
}
}
}
}
}Restart the gateway:
openclaw gateway restartConfig fields
| Field | Required | Default | Description |
|-------|----------|---------|-------------|
| url | no | nats://localhost:4222 | NATS server URL |
| agentName | yes | - | 4th subject token (agents.oc.<owner>.<agentName>) |
| description | no | OpenClaw agent <agentName> | Shown via $SRV.INFO |
| credentials | no | - | Path to .creds file for NATS authentication |
| owner | no | default | 3rd subject token - operator/account namespace. Spec §2 requires a 4-token subject. |
Migrating from v0.1: the old
orgfield has been renamedowner(§3.2 terminology). The old name is still accepted as an alias with a deprecation warning in logs.
Environment variables (Docker / containers)
All fields can be overridden via env vars:
| Env Var | Overrides | Example |
|---------|-----------|---------|
| NATS_URL | url | nats://prod.example.com:4222 |
| NATS_AGENT_NAME | agentName | my-agent |
| NATS_DESCRIPTION | description | Production agent |
| NATS_OWNER | owner | acme |
| NATS_ORG | owner (legacy alias) | acme |
| NATS_CREDENTIALS | credentials | /run/secrets/nats.creds |
# docker-compose.yml
environment:
NATS_AGENT_NAME: my-agent
NATS_URL: nats://nats:4222
NATS_DESCRIPTION: Production agent
NATS_OWNER: acmeVerify
# Protocol-level discovery
nats req '$SRV.INFO.agents' '' --replies=0 --timeout=2s
# Micro service listing
nats micro list
nats micro info agents
# Watch heartbeats
nats sub 'agents.*.*.*.heartbeat'Talking to a running OpenClaw agent
Any caller speaking the protocol - a spec-compliant SDK or the nats CLI - can:
# Plain text prompt
nats req agents.oc.<owner>.<agentName> "Hello!" --wait-for-empty --timeout 60s
# JSON envelope (the SDK form)
nats req agents.oc.<owner>.<agentName> '{"prompt":"Hello!"}' --wait-for-empty --timeout 60s
# With an attachment
nats req agents.oc.<owner>.<agentName> '{
"prompt": "describe this image",
"attachments": [{"filename":"pic.png","content":"<base64>"}]
}' --wait-for-empty --timeout 120sWith the TypeScript SDK:
import { connect } from "@nats-io/transport-node";
import { Agents } from "@synadia/agents";
const nc = await connect({ servers: "nats://localhost:4222" });
const agents = new Agents({ nc });
const [agent] = await agents.discover({ filter: { agent: "openclaw" } });
for await (const msg of await agent!.prompt("what can you do?")) {
if (msg.type === "response") process.stdout.write(msg.text);
}
await agents.close();
await nc.close();Wire protocol (summary)
Full spec: https://github.com/synadia-ai/nats-agent-sdk-docs. Quick reference:
- Request: plain UTF-8 text OR JSON
{"prompt":"…","attachments":[{"filename":"…","content":"<base64>"},…]}. Attachmentcontentmust be RFC 4648 §4 base64 (standard alphabet, padded, no URL-safe variant, no whitespace). - Response: one or more typed chunks on the reply subject:
{"type":"response","data":"<text>"}- content{"type":"status","data":"ack"}- accepted / keep-alive
- Terminator: empty body and no headers (§6.5).
- Errors:
Nats-Service-Error-Codeheader with400/500, followed by the terminator.
Attachments
When a request envelope contains attachments, each file is decoded and staged at:
~/.openclaw/attachments/<agentName>/<uuid>/<filename>The absolute paths are prepended to the prompt as:
[Attachments available at the following absolute paths]
- /home/you/.openclaw/attachments/my-agent/abcd-…/pic.png
<original prompt text>OpenClaw's dispatch pipeline sees the list in the user message and the agent can open the files with its file tools. The whole <agentName> directory is removed when the gateway stops; within a gateway lifetime, attachments from earlier prompts remain on disk so follow-up turns can reference them.
Caller-side constraints (rejected with 400 if violated):
contentmust be strict RFC 4648 §4 base64 - standard alphabet, padded, no URL-safe, no whitespace.filenamemust be a plain basename. Path separators (/,\),.., absolute paths, and NUL bytes are rejected rather than silently flattened.- Full encoded envelope must fit within
max_payload(1 MB).
Spec §5.5 reserves a future attachments endpoint at agents.oc.<owner>.<agentName>.attachments for chunked large-file upload; that lands in protocol 0.2 and will coexist with inline attachments.
Agent-initiated messages (OpenClaw-specific)
When OpenClaw's outbound sendText fires, the channel publishes to:
agents.oc.<owner>.<agentName>.outboundThis is a pub/sub subject (fire-and-forget), not part of the spec. External listeners can subscribe with nats sub agents.oc.<owner>.<agentName>.outbound. The subject is deliberately under the agent root so it's easy to locate relative to the prompt subject.
Tenant isolation
The spec reserves the four-token subject structure; there is no additional namespace slot. For multi-tenant isolation, use NATS accounts and subject permissions (spec §10.1). Within an account, agents with distinct owner tokens coexist cleanly.
Discovery
Spec-compliant SDKs discover via $SRV.PING.agents / $SRV.INFO.agents. No custom .inspect endpoint (the pre-0.3 channel had one; it's gone - $SRV.INFO replaces it).
Troubleshooting
[nats] config field 'org' is deprecated. Renameorg→ownerin youropenclaw.json. The old name still works but the warning will stay until you update.- Gateway fails with
NATS: disconnected. Check the configured URL and, if using credentials, that the.credsfile exists and is readable. nats reqreturns nothing or hangs. Pass--wait-for-empty; the protocol signals end-of-stream with an empty-body message, not a single response.400 attachment[N] has invalid base64 content. The caller emitted URL-safe base64 or unpadded output. Switch to RFC 4648 §4 (standard alphabet, padded) - Node'sBuffer.from(bytes).toString("base64")produces the correct form.400 attachment[N] has unsafe filename. Send the basename only (e.g."pic.png", not"./images/pic.png").
Development
bun install
bun run test # protocol unit tests (no nats-server required)
bun run test:smoke # wire-level smoke against nats-server on 127.0.0.1:4222The smoke test drives a minimal spec-compliant service assembled from the repo's own protocol.ts + attachments.ts and verifies $SRV.INFO shape, heartbeat fields, four 400 paths, the ack → response → terminator cycle, and attachment staging + cleanup.
License
MIT
