npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@zf-tech/openclaw-websocket

v0.2.11

Published

OpenClaw WebSocket chat channel plugin - Connect your apps to OpenClaw via WebSocket

Downloads

440

Readme

openclaw-websocket

npm version License: MIT

OpenClaw WebSocket channel plugin for talking to OpenClaw agents over a single JSON-over-WebSocket protocol.

This plugin is now WebSocket-only. Standalone HTTP SSE and SSE-style frames over WebSocket are not supported anymore.

README.md is the source of truth. README_CN.md may lag behind.

Features

  • Real-time bidirectional chat over WebSocket
  • Streaming replies with chat.stream and final replies with chat.response
  • Optional external authentication on connection
  • Direct-message access control with open, allowlist, pairing, or disabled
  • Optional dynamic agent creation for one-agent-per-user setups
  • Optional Redis-backed session state with in-memory fallback
  • Direct and group chat routing
  • Reply quotes and media-path metadata pass-through to the agent context

Important Behavior

  • The protocol is JSON over WebSocket only.
  • Connection identity is fixed at connect time.
  • If authentication succeeds, the connection identity comes from the auth service response.
  • chat.send.senderId and chat.send.senderName are accepted for backward compatibility but ignored.
  • Dynamic agent creation is disabled by default.

Requirements

  • OpenClaw 2026.4.8 or newer

Installation

# Install from npm
openclaw plugins install @zf-tech/openclaw-websocket

# Or install from GitHub
openclaw plugins install github:zf-tech/openclaw-websocket

Updating

Prefer OpenClaw's built-in update flow:

openclaw plugins update openclaw-websocket

Avoid deleting the plugin directory manually before reinstalling. That can leave you in a temporary invalid-config state where channels.websocket exists in config but the channel plugin is not loaded yet.

Quick Start

Local Development Without Auth

For local testing, disable auth explicitly:

{
  "channels": {
    "websocket": {
      "enabled": true,
      "host": "0.0.0.0",
      "port": 18800,
      "path": "/ws",
      "sessionRouting": {
        "directMode": "per-connection"
      },
      "auth": {
        "enabled": false,
        "required": false
      }
    }
  }
}

Start OpenClaw:

openclaw start

Connect with wscat:

npm install -g wscat
wscat -c "ws://127.0.0.1:18800/ws?senderId=user1&senderName=John"

Then send:

{"type":"chat.send","content":"Hello!"}

On a successful connection the server sends a welcome chat.response.

Production With Auth

Auth is enabled by default. A typical production config looks like this:

{
  "channels": {
    "websocket": {
      "enabled": true,
      "host": "0.0.0.0",
      "port": 18800,
      "path": "/ws",
      "sessionRouting": {
        "directMode": "per-connection"
      },
      "auth": {
        "enabled": true,
        "endpoint": "http://localhost:3000/api/auth/verify",
        "timeout": 5000,
        "required": true
      }
    }
  }
}

Connect with a token:

ws://127.0.0.1:18800/ws?token=YOUR_JWT_TOKEN

Full Configuration Example

{
  "channels": {
    "websocket": {
      "enabled": true,
      "host": "0.0.0.0",
      "port": 18800,
      "path": "/ws",
      "dmPolicy": "open",
      "allowFrom": [],
      "auth": {
        "enabled": true,
        "endpoint": "http://localhost:3000/api/auth/verify",
        "timeout": 5000,
        "required": true
      },
      "stream": {
        "partialDebounceMs": 80
      },
      "dynamicAgentCreation": {
        "enabled": false,
        "workspaceTemplate": "~/.openclaw/workspace-{agentId}",
        "agentDirTemplate": "~/.openclaw/agents/{agentId}/agent",
        "maxAgents": 100
      },
      "redis": {
        "enabled": false,
        "url": "redis://127.0.0.1:6379",
        "keyPrefix": "openclaw:websocket",
        "sessionTtlSeconds": 86400
      },
      "limits": {
        "maxConnections": 1000,
        "maxInboundMessageBytes": 2097152,
        "maxWsPayloadBytes": 2097152,
        "maxWsBufferedAmountBytes": 16777216,
        "dropNonEssentialWhenBufferedOverBytes": 8388608,
        "inboundRateLimit": {
          "maxPerMinute": 600,
          "burst": 60
        }
      }
    }
  }
}

Configuration Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | enabled | boolean | true | Enable or disable the channel | | host | string | "0.0.0.0" | Bind address | | port | number | 18800 | WebSocket listen port | | path | string | "/ws" | WebSocket endpoint path | | dmPolicy | "open" \| "allowlist" \| "pairing" \| "disabled" | "open" | Direct-message access control | | allowFrom | string[] | [] | Sender allowlist used by allowlist and pairing modes | | auth.enabled | boolean | true | Enable external auth | | auth.endpoint | string | "http://localhost:3000/api/auth/verify" | Auth service verify URL | | auth.timeout | number | 5000 | Auth request timeout in milliseconds | | auth.required | boolean | true | Reject missing or invalid tokens when true | | stream.partialDebounceMs | number | 80 | Debounce partial stream chunks before sending chat.stream | | dynamicAgentCreation.enabled | boolean | false | Auto-create a dedicated agent for an unmatched direct-message sender | | dynamicAgentCreation.workspaceTemplate | string | "~/.openclaw/workspace-{agentId}" | Workspace path template when dynamic agents are enabled | | dynamicAgentCreation.agentDirTemplate | string | "~/.openclaw/agents/{agentId}/agent" | Agent directory template when dynamic agents are enabled | | dynamicAgentCreation.maxAgents | number | - | Hard cap for auto-created agents | | redis.enabled | boolean | false | Enable Redis-backed session state | | redis.url | string | "redis://127.0.0.1:6379" | Redis connection URL | | redis.keyPrefix | string | "openclaw:websocket" | Redis key prefix | | redis.sessionTtlSeconds | number | 86400 | Session retention TTL | | limits.maxConnections | number | - | Maximum concurrent WebSocket connections | | limits.maxInboundMessageBytes | number | 2097152 | Maximum inbound message size in bytes | | limits.maxWsPayloadBytes | number | 2097152 | ws server maxPayload limit | | limits.maxWsBufferedAmountBytes | number | 16777216 | Hard outbound buffer limit; connection closes when exceeded | | limits.dropNonEssentialWhenBufferedOverBytes | number | 8388608 | Drop chat.typing and chat.stream when the buffer is above this threshold | | limits.inboundRateLimit.maxPerMinute | number | 600 | Token refill rate per minute | | limits.inboundRateLimit.burst | number | 60 | Token bucket burst capacity |

Authentication

The plugin calls auth.endpoint once per connection and sends all WebSocket query parameters as JSON.

If the client connects to:

ws://127.0.0.1:18800/ws?token=abc123&tokenType=local&senderId=ignored-when-auth-succeeds

The auth service receives:

{
  "token": "abc123",
  "tokenType": "local",
  "senderId": "ignored-when-auth-succeeds"
}

If tokenType is omitted, the plugin adds "tokenType": "local".

Expected auth response:

{
  "success": true,
  "data": {
    "userId": "user_123",
    "username": "John"
  }
}

Behavior:

  • When auth succeeds, userId and username become the connection identity.
  • When auth.required is true, missing or invalid tokens close the connection.
  • When auth.required is false, missing or invalid tokens fall back to an anonymous connection.

Environment Variables

The plugin currently supports these auth overrides:

| Variable | Description | |----------|-------------| | WS_AUTH_ENABLED | Override auth.enabled with true / false / 1 / 0 | | WS_AUTH_ENDPOINT | Override auth.endpoint |

Example:

WS_AUTH_ENABLED=true \
WS_AUTH_ENDPOINT=http://auth-service:3000/api/auth/verify \
openclaw start

Connection Identity

Identity is set once, at connection time.

  • With successful auth: identity comes from the auth response.
  • Without successful auth: identity comes from URL query params senderId and senderName.
  • If auth is disabled and senderId is omitted, the server generates a random connection ID, so reconnects will not resume the same direct-message session.

Legacy inbound fields senderId, senderName, and connectionId are accepted for backward compatibility, but they do not change identity. sessionKey is now a first-class routing input when provided.

Direct-Message Access Control

dmPolicy applies to direct messages only. Group messages still route by groupId.

  • open: allow all direct messages
  • allowlist: allow only senders in allowFrom or in the pairing approval store
  • pairing: unknown senders receive a pairing code and their original message is not processed
  • disabled: block all direct messages

Matching rules:

  • Allowlist entries are exact sender IDs.
  • * matches any sender.
  • When auth succeeds, the sender ID used for policy checks is the authenticated userId.

Pairing Workflow

When dmPolicy is pairing, the plugin uses OpenClaw's standard pairing store for the websocket channel.

List pending requests:

openclaw pairing list websocket

Approve a code:

openclaw pairing approve websocket <CODE>

Once approved, that sender can chat without receiving another pairing prompt.

Dynamic Agent Creation

When enabled, unmatched direct-message senders can get their own dedicated agent and workspace.

{
  "channels": {
    "websocket": {
      "dynamicAgentCreation": {
        "enabled": true,
        "workspaceTemplate": "~/.openclaw/workspace-{agentId}",
        "agentDirTemplate": "~/.openclaw/agents/{agentId}/agent",
        "maxAgents": 100
      }
    }
  }
}

Template variables:

  • {agentId}: generated stable agent ID prefixed with ws-
  • {userId}: filesystem-safe sender ID token
  • {userIdSafe}: same as {userId}; kept as an explicit alias

Notes:

  • Unsafe characters in sender IDs are sanitized before they are used in file paths.
  • A short hash is added when needed to avoid collisions.
  • Raw sender IDs are still used for routing and bindings; sanitized values are only for agent IDs and filesystem paths.

Session Behavior

  • Direct-message sessions are keyed by the resolved sender identity.
  • You can switch direct-message routing to per-connection mode with channels.websocket.sessionRouting.directMode = "per-connection".
  • Group sessions are keyed by groupId.
  • One user's direct-message history is separate from another user's.
  • Group messages in the same groupId share the same session route.
  • If a message includes sessionKey, that session key overrides the default sender/group-derived route for this WebSocket conversation.

Message Protocol

WebSocket URL Parameters

| Parameter | Required | Description | |-----------|----------|-------------| | token | No | Auth token; required when auth.enabled=true and auth.required=true | | tokenType | No | Optional auth hint; defaults to local | | senderId | No | Connection identity when auth does not provide one | | senderName | No | Connection display name when auth does not provide one |

Send Messages

{
  "type": "chat.send",
  "messageId": "msg_001",
  "sessionKey": "agent:cpq-agent:websocket:direct:chat-tab-1",
  "content": "Hello!",
  "chatType": "direct",
  "replyToBody": "Previous message text",
  "mediaPath": "/path/to/file.png",
  "mediaType": "image/png",
  "mediaPaths": ["/path/a.png", "/path/b.png"],
  "mediaTypes": ["image/png", "image/png"],
  "customData": {
    "priority": "high",
    "source": "api"
  }
}

Fields:

| Field | Type | Required | Description | |-------|------|----------|-------------| | type | string | Yes | Must be "chat.send" | | messageId | string | No | Client-supplied message ID; auto-generated if omitted | | sessionKey | string | No | Explicit OpenClaw session key used for routing this conversation | | content | string | Yes | Non-empty message text | | chatType | "direct" \| "group" | No | Defaults to "direct" | | groupId | string | Group only | Required when chatType="group" | | groupSubject | string | No | Human-readable group label | | replyToMessageId | string | No | Reply target ID | | replyToBody | string | No | Reply quote text included in the agent envelope | | mediaPath | string | No | Single media path forwarded to the agent context | | mediaType | string | No | MIME type for mediaPath | | mediaPaths | string[] | No | Multiple media paths forwarded to the agent context | | mediaTypes | string[] | No | MIME types for mediaPaths | | customData | object | No | Custom context data forwarded as CustomData; non-reserved keys are also preserved for compatibility |

Notes:

  • Message-level identity overrides are ignored.
  • sessionKey changes routing, but does not change sender identity.
  • If you do not send sessionKey, direct-message routing still follows sessionRouting.directMode.
  • Group messages without groupId are rejected.
  • customData cannot overwrite reserved system context fields.
  • Media fields are pass-through metadata and file-path hints only; this plugin does not upload or host media blobs.

Receive Messages

// Typing indicator
{ "type": "chat.typing" }

// Partial streaming chunk
{ "type": "chat.stream", "messageId": "msg_001", "content": "Partial...", "done": false }

// Final reply
{ "type": "chat.response", "messageId": "msg_001", "content": "Complete reply", "done": true }

// Error
{ "type": "chat.error", "messageId": "msg_001", "error": "Error message" }

Response types:

| Type | Description | |------|-------------| | chat.typing | Agent reply has started | | chat.stream | Partial streaming content | | chat.response | Final or informational response | | chat.error | Validation, access-control, auth, or processing error |

Example Flows

Direct Message

wscat -c "ws://127.0.0.1:18800/ws?senderId=alice&senderName=Alice"
{"type":"chat.send","content":"My name is Alice"}
{"type":"chat.send","content":"What is my name?"}

Group Message

{"type":"chat.send","content":"Hello team","chatType":"group","groupId":"dev-team","groupSubject":"Dev Team"}

Pairing-Protected DM

{"type":"chat.send","content":"Hello"}

If dmPolicy is pairing and the sender is unknown, the plugin replies with a pairing code and does not process the original message until the code is approved.

Code Examples

JavaScript / Node.js

const WebSocket = require("ws");

const ws = new WebSocket("ws://127.0.0.1:18800/ws?senderId=user1&senderName=John");

ws.on("open", () => {
  console.log("Connected");
  ws.send(JSON.stringify({
    type: "chat.send",
    content: "Hello, how are you?"
  }));
});

ws.on("message", (data) => {
  const msg = JSON.parse(data.toString());
  switch (msg.type) {
    case "chat.typing":
      console.log("Agent is typing...");
      break;
    case "chat.stream":
      process.stdout.write(msg.content ?? "");
      break;
    case "chat.response":
      console.log("\nAgent:", msg.content);
      break;
    case "chat.error":
      console.error("Error:", msg.error);
      break;
  }
});

Python

import json
import websocket

def on_message(ws, message):
    msg = json.loads(message)
    if msg["type"] == "chat.typing":
        print("Agent is typing...")
    elif msg["type"] == "chat.stream":
        print(msg.get("content", ""), end="")
    elif msg["type"] == "chat.response":
        print("\nAgent:", msg.get("content"))
    elif msg["type"] == "chat.error":
        print("Error:", msg.get("error"))

def on_open(ws):
    ws.send(json.dumps({
        "type": "chat.send",
        "content": "Hello!"
    }))

ws = websocket.WebSocketApp(
    "ws://127.0.0.1:18800/ws?senderId=user1&senderName=John",
    on_open=on_open,
    on_message=on_message,
)
ws.run_forever()

Redis Session Store

If Redis is enabled, session state is written to Redis with the configured TTL. If Redis cannot be reached, the plugin logs the error and falls back to in-memory storage.

Migration Notes

If you are upgrading from an older build of this plugin:

  • Remove stream.mode
  • Remove sse.*
  • Remove limits.maxSseBodyBytes
  • Remove limits.maxSseBufferedAmountBytes
  • Update clients to consume plain JSON WebSocket messages only
  • Stop relying on message-level senderId and senderName overrides
  • If you use dynamic agent creation, note that it is now disabled by default

Contributing

Pull requests are welcome.

License

MIT