Add client and plugin specifications

This commit is contained in:
root
2026-03-31 21:36:32 +02:00
parent f8bc254d30
commit 8dd07e791c
2 changed files with 1033 additions and 0 deletions

633
docs/CLIENT_SPEC.md Normal file
View File

@@ -0,0 +1,633 @@
# WebSocket Client Specification
## Overview
This document describes how to build a **client application** that connects to the OpenClaw WebSocket Channel plugin.
**Server URL:** `ws://your-openclaw-host:8765/ws` (configurable)
**Authentication:** API Key via `X-API-Key` header or `?key=` query parameter
---
## Quick Start
### JavaScript/Node.js Example
```javascript
import WebSocket from 'ws';
const ws = new WebSocket('ws://localhost:8765/ws?key=YOUR_API_KEY');
ws.on('open', () => {
console.log('Connected to OpenClaw!');
});
ws.on('message', (data) => {
const message = JSON.parse(data.toString());
console.log('Received:', message);
switch (message.type) {
case 'handshake':
console.log('Server capabilities:', message.capabilities);
break;
case 'message':
console.log('Bot says:', message.content.text);
break;
}
});
// Send a message to the bot
function sendMessage(text) {
ws.send(JSON.stringify({
type: 'message',
from: 'user-123', // Your user ID
content: {
type: 'text',
text: text
}
}));
}
sendMessage('Hello, OpenClaw!');
```
### Python Example
```python
import websocket
import json
import threading
def on_message(ws, message):
data = json.loads(message)
print(f"Received: {data}")
if data['type'] == 'handshake':
print(f"Capabilities: {data['capabilities']}")
elif data['type'] == 'message':
print(f"Bot: {data['content']['text']}")
def on_open(ws):
print("Connected!")
# Send a test message
ws.send(json.dumps({
"type": "message",
"from": "user-123",
"content": {"type": "text", "text": "Hello!"}
}))
def on_error(ws, error):
print(f"Error: {error}")
def on_close(ws, close_status_code, close_msg):
print("Disconnected")
# Connect
ws = websocket.WebSocketApp(
"ws://localhost:8765/ws?key=YOUR_API_KEY",
on_message=on_message,
on_open=on_open,
on_error=on_error,
on_close=on_close
)
ws.run_forever()
```
---
## Connection
### URL Format
```
ws://<host>:<port>/<path>?key=<api_key>
```
**Default values:**
- Host: `localhost`
- Port: `8765`
- Path: `/ws`
### Authentication
Provide API key in one of two ways:
**1. Query Parameter (recommended for WebSocket):**
```
ws://localhost:8765/ws?key=sk_test_abc123
```
**2. HTTP Header (during upgrade):**
```javascript
const ws = new WebSocket('ws://localhost:8765/ws', {
headers: {
'X-API-Key': 'sk_test_abc123'
}
});
```
### Connection Response
**Success:** Connection established, server sends handshake message.
**Failure:** Connection closed with error code:
- `1008` - Invalid/missing API key
- `1003` - No accounts configured
---
## Message Protocol
### Server → Client
#### 1. Handshake (sent immediately on connect)
Sent once when connection is established.
```json
{
"type": "handshake",
"accountId": "default",
"capabilities": {
"text": true,
"images": false,
"files": false,
"voice": false,
"reactions": false,
"threads": false,
"typing": false
},
"metadata": {
"serverName": "Production",
"region": "eu-west"
},
"timestamp": 1703275200000
}
```
**Fields:**
- `capabilities` - What this account supports
- `metadata` - Custom metadata from config
---
#### 2. Text Message
```json
{
"type": "message",
"to": "user-123",
"content": {
"type": "text",
"text": "Hello! How can I help?"
},
"timestamp": 1703275201000,
"messageId": "msg_abc123",
"threadId": "thread_xyz" // optional
}
```
---
#### 3. Image Message
```json
{
"type": "message",
"to": "user-123",
"content": {
"type": "image",
"url": "https://example.com/image.png",
"alt": "Description of image"
},
"timestamp": 1703275202000,
"messageId": "msg_def456"
}
```
---
#### 4. File Message
```json
{
"type": "message",
"to": "user-123",
"content": {
"type": "file",
"url": "https://example.com/document.pdf",
"name": "document.pdf",
"size": 1024567
},
"timestamp": 1703275203000,
"messageId": "msg_ghi789"
}
```
---
#### 5. Voice Message
```json
{
"type": "message",
"to": "user-123",
"content": {
"type": "voice",
"url": "https://example.com/audio.ogg",
"duration": 45.2
},
"timestamp": 1703275204000,
"messageId": "msg_jkl012"
}
```
---
#### 6. Typing Indicator
```json
{
"type": "typing",
"to": "user-123",
"isTyping": true
}
```
When `isTyping: false` — bot stopped typing.
---
#### 7. Pairing Code Request
```json
{
"type": "pairing_code",
"code": "ABC123",
"target": "user-456",
"message": "Send this code to verify your identity:"
}
```
Used for DM pairing flow.
---
### Client → Server
#### 1. Text Message
```json
{
"type": "message",
"from": "user-123",
"content": {
"type": "text",
"text": "Hello, bot!"
},
"threadId": "thread_xyz", // optional, for threaded conversations
"timestamp": 1703275200000 // optional, server will add if missing
}
```
**Required fields:**
- `type` - Must be `"message"`
- `from` - Your user ID (must be in account's `allowFrom` list if DM policy is `allowlist`)
- `content.type` - Message type (`"text"`, `"image"`, `"file"`, `"voice"`)
- `content.text` - For text messages
---
#### 2. Image Message
```json
{
"type": "message",
"from": "user-123",
"content": {
"type": "image",
"url": "https://example.com/user-image.png",
"alt": "User uploaded image"
}
}
```
**Note:** Only works if account has `capabilities.images: true`.
---
#### 3. File Message
```json
{
"type": "message",
"from": "user-123",
"content": {
"type": "file",
"url": "https://example.com/user-file.pdf",
"name": "my-document.pdf"
}
}
```
**Note:** Only works if account has `capabilities.files: true`.
---
#### 4. Voice Message
```json
{
"type": "message",
"from": "user-123",
"content": {
"type": "voice",
"url": "https://example.com/user-voice.ogg",
"duration": 30.5
}
}
```
**Note:** Only works if account has `capabilities.voice: true`.
---
## Capabilities
The server advertises capabilities during handshake. **Only send message types that are enabled:**
| Capability | Can Send | Can Receive |
|------------|----------|-------------|
| `text: true` | ✅ Text messages | ✅ Text messages |
| `images: true` | ✅ Image messages | ✅ Image messages |
| `files: true` | ✅ File messages | ✅ File messages |
| `voice: true` | ✅ Voice messages | ✅ Voice messages |
| `reactions: true` | ❌ (future) | ❌ (future) |
| `threads: true` | ✅ With `threadId` | ✅ With `threadId` |
| `typing: true` | ❌ (future) | ✅ Typing indicators |
**Important:** If you send a message type that's not enabled, the server will log a warning and drop the message.
---
## DM Security
The server enforces DM policies per account:
### `allowlist` (default)
- Only users in `allowFrom` list can send messages
- Check handshake metadata or config for allowed users
### `open`
- Any user can send messages
- No restrictions
### `closed`
- No DMs allowed
- All messages rejected
---
## Threading
If `capabilities.threads: true`, you can use threaded conversations:
```json
{
"type": "message",
"from": "user-123",
"content": {"type": "text", "text": "Reply in thread"},
"threadId": "unique-thread-id"
}
```
**Thread ID rules:**
- Must be consistent for related messages
- Can be any string (UUID, conversation ID, etc.)
- Server uses this to group messages
---
## Error Handling
### Invalid JSON
Server responds with error message:
```json
{
"type": "error",
"error": "Invalid message format"
}
```
### Unsupported Message Type
```json
{
"type": "error",
"error": "Unsupported message type: xyz"
}
```
### Capability Disabled
Message is silently dropped (check server logs).
### Connection Lost
Implement reconnection logic:
```javascript
ws.on('close', () => {
console.log('Disconnected, reconnecting...');
setTimeout(() => connect(), 5000);
});
```
---
## Complete Example: Chat Client
```javascript
import WebSocket from 'ws';
class OpenClawClient {
constructor(url, apiKey, userId) {
this.url = url;
this.apiKey = apiKey;
this.userId = userId;
this.ws = null;
this.capabilities = null;
this.messageHandlers = [];
}
connect() {
return new Promise((resolve, reject) => {
const wsUrl = `${this.url}?key=${this.apiKey}`;
this.ws = new WebSocket(wsUrl);
this.ws.on('open', () => {
console.log('Connected to OpenClaw');
});
this.ws.on('message', (data) => {
const message = JSON.parse(data.toString());
this.handleMessage(message);
});
this.ws.on('error', (error) => {
reject(error);
});
this.ws.on('close', () => {
console.log('Disconnected, reconnecting in 5s...');
setTimeout(() => this.connect(), 5000);
});
});
}
handleMessage(message) {
switch (message.type) {
case 'handshake':
this.capabilities = message.capabilities;
console.log('Server capabilities:', message.capabilities);
break;
case 'message':
// Bot sent a message
this.messageHandlers.forEach(h => h(message));
if (message.content.type === 'text') {
console.log('Bot:', message.content.text);
}
break;
case 'typing':
if (message.isTyping) {
console.log('Bot is typing...');
}
break;
case 'error':
console.error('Server error:', message.error);
break;
}
}
onMessage(handler) {
this.messageHandlers.push(handler);
}
send(text) {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
throw new Error('Not connected');
}
this.ws.send(JSON.stringify({
type: 'message',
from: this.userId,
content: {
type: 'text',
text: text
}
}));
}
disconnect() {
if (this.ws) {
this.ws.close();
}
}
}
// Usage
const client = new OpenClawClient(
'ws://localhost:8765/ws',
'YOUR_API_KEY',
'user-123'
);
client.connect().then(() => {
client.onMessage((msg) => {
console.log('Received:', msg.content.text);
});
client.send('Hello, OpenClaw!');
});
```
---
## Testing
### Using wscat
```bash
# Install
npm install -g wscat
# Connect
wscat -c "ws://localhost:8765/ws?key=YOUR_API_KEY"
# Send message
> {"type":"message","from":"user-123","content":{"type":"text","text":"Hello!"}}
```
### Using Python websocket-client
```bash
pip install websocket-client
python test_client.py
```
---
## Configuration Example
Server-side config (`openclaw.json`):
```json
{
"channels": {
"websocket": {
"port": 8765,
"path": "/ws",
"accounts": [
{
"accountId": "my-app",
"apiKey": "sk_test_abc123",
"allowFrom": ["user-123", "user-456"],
"dmPolicy": "allowlist",
"capabilities": {
"text": true,
"images": true,
"files": false,
"voice": false,
"reactions": false,
"threads": true,
"typing": true
},
"metadata": {
"appName": "My Chat App",
"version": "1.0.0"
}
}
]
}
}
}
```
---
## Security Best Practices
1. **Use HTTPS/WSS in production** - Always use secure WebSocket (`wss://`) in production
2. **Rotate API keys** - Change API keys periodically
3. **Validate user IDs** - Ensure `from` field matches expected users
4. **Rate limiting** - Implement client-side rate limiting
5. **Error handling** - Don't expose internal errors to users
---
## Support
For issues or questions:
- OpenClaw Docs: https://docs.openclaw.ai
- Discord: https://discord.com/invite/clawd
- GitHub: https://github.com/openclaw/openclaw

400
docs/PLUGIN_SPEC.md Executable file
View File

@@ -0,0 +1,400 @@
# WebSocket Channel Plugin Specification
## Overview
Build an OpenClaw channel plugin that enables custom messaging platforms to connect via WebSocket. The plugin provides a flexible, configurable foundation where each channel instance can define its own capabilities and requirements.
**Plugin Name:** `openclaw-websocket-channel`
**Plugin ID:** `websocket-channel`
**Channel ID:** `websocket`
---
## Architecture Options
### Option A: WebSocket Server Inside Gateway (Recommended)
The WebSocket server runs as part of the OpenClaw Gateway process.
**Pros:**
- Single deployment unit — no separate service to manage
- Direct access to OpenClaw runtime APIs (TTS, STT, subagents)
- Lower latency — no network hop between gateway and websocket server
- Unified logging and diagnostics
- Easier authentication via gateway's existing auth system
**Cons:**
- Gateway must expose WebSocket port publicly (or via reverse proxy)
- All WebSocket connections terminate on the gateway host
- Less isolation — buggy WebSocket code could affect gateway stability
**Best for:** Most use cases, especially when you control the gateway deployment.
### Option B: Separate WebSocket Service
A standalone WebSocket service connects to OpenClaw via HTTP routes or a sidecar pattern.
**Pros:**
- Isolated failure domain — WebSocket issues don't crash gateway
- Can scale WebSocket connections independently
- Easier to deploy WebSocket server closer to clients (edge)
**Cons:**
- More complex deployment (two services to manage)
- Need secure channel between WebSocket service and Gateway
- Additional latency for message routing
- Must implement custom bridging logic
**Best for:** High-scale deployments or when WebSocket server must run in a different network zone.
---
## Recommendation
**Use Option A** (WebSocket server inside Gateway) for this plugin. It's simpler, more integrated, and matches the pattern used by other channel plugins (Discord, Telegram, etc.).
---
## Technical Requirements
### 1. Package Structure
```
extensions/websocket-channel/
├── package.json # openclaw.channel metadata
├── openclaw.plugin.json # Manifest with config schema
├── index.ts # defineChannelPluginEntry
├── setup-entry.ts # defineSetupPluginEntry
└── src/
├── channel.ts # WebSocketChannelPlugin
├── websocket-server.ts # WebSocket server implementation
├── client.ts # WebSocket client connection handler
├── auth.ts # API key authentication
├── config.ts # Channel configuration types
└── channel.test.ts # Tests
```
### 2. Manifest (`openclaw.plugin.json`)
```json
{
"id": "websocket-channel",
"kind": "channel",
"channels": ["websocket"],
"name": "WebSocket Channel",
"description": "Custom WebSocket-based messaging channel for OpenClaw",
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {
"websocket": {
"type": "object",
"properties": {
"port": { "type": "number", "default": 8765 },
"path": { "type": "string", "default": "/ws" },
"accounts": {
"type": "array",
"items": {
"type": "object",
"properties": {
"accountId": { "type": "string" },
"apiKey": { "type": "string" },
"allowFrom": {
"type": "array",
"items": { "type": "string" }
},
"dmPolicy": {
"type": "string",
"enum": ["allowlist", "open", "closed"],
"default": "allowlist"
},
"capabilities": {
"type": "object",
"properties": {
"text": { "type": "boolean", "default": true },
"images": { "type": "boolean", "default": false },
"files": { "type": "boolean", "default": false },
"voice": { "type": "boolean", "default": false },
"reactions": { "type": "boolean", "default": false },
"threads": { "type": "boolean", "default": false },
"typing": { "type": "boolean", "default": false }
}
},
"metadata": {
"type": "object",
"description": "Custom metadata exposed to clients"
}
},
"required": ["accountId", "apiKey"]
}
}
},
"required": ["accounts"]
}
},
"required": ["websocket"]
}
}
```
### 3. Channel Plugin Implementation
#### Core Interface
```typescript
import {
createChatChannelPlugin,
createChannelPluginBase,
} from "openclaw/plugin-sdk/core";
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
type WebSocketAccount = {
accountId: string | null;
apiKey: string;
allowFrom: string[];
dmPolicy: string;
capabilities: ChannelCapabilities;
metadata?: Record<string, any>;
};
type ChannelCapabilities = {
text: boolean;
images: boolean;
files: boolean;
voice: boolean;
reactions: boolean;
threads: boolean;
typing: boolean;
};
```
#### WebSocket Server
- Use `ws` package (or native Bun WebSocket if available)
- Listen on configurable port/path from config
- Support multiple accounts (multi-tenant)
- Authenticate connections via `X-API-Key` header or query param
#### Message Protocol
Define a JSON message format for WebSocket communication:
**Client → Server (inbound):**
```json
{
"type": "message",
"accountId": "account-123",
"from": "user-456",
"content": {
"type": "text",
"text": "Hello!"
},
"timestamp": 1703275200000
}
```
**Server → Client (outbound):**
```json
{
"type": "message",
"to": "user-456",
"content": {
"type": "text",
"text": "Hi there!"
},
"timestamp": 1703275201000
}
```
**Server → Client (capabilities advertisement):**
```json
{
"type": "handshake",
"accountId": "account-123",
"capabilities": {
"text": true,
"images": true,
"files": false,
"voice": false,
"reactions": true,
"threads": true,
"typing": true
},
"metadata": {
"serverVersion": "1.0.0",
"customField": "value"
}
}
```
#### Authentication
- API key passed via `X-API-Key` header or `?key=` query param
- Validate against configured accounts
- Reject connections with invalid/missing keys
- Log authentication failures
#### DM Security
- Implement `security.dm` adapter
- Support `allowlist`, `open`, and `closed` policies
- Per-account allowFrom lists
#### Pairing
- Implement text-based pairing flow
- Send pairing code via WebSocket to client
- Client must return code to complete pairing
#### Threading
- Support both `reply` and `thread` modes
- Configurable per account or fixed
#### Outbound Messaging
- `sendText`: Send text messages via WebSocket
- `sendMedia`: Send file paths (client must fetch)
- Support typing indicators if capability enabled
### 4. HTTP Route Registration
Register WebSocket upgrade route in Gateway:
```typescript
registerFull(api) {
api.registerHttpRoute({
path: "/ws",
auth: "plugin",
handler: async (req, res) => {
// Handle WebSocket upgrade
// Delegate to websocket-server.ts
},
});
}
```
### 5. Capabilities Advertisement
On connection handshake, send capabilities to client:
```typescript
async function sendHandshake(ws, account) {
ws.send(JSON.stringify({
type: "handshake",
accountId: account.accountId,
capabilities: account.capabilities,
metadata: account.metadata || {}
}));
}
```
This allows clients to adapt their UI/behavior based on supported features.
### 6. Configuration Examples
#### Minimal Config
```json5
{
"channels": {
"websocket": {
"port": 8765,
"path": "/ws",
"accounts": [
{
"accountId": "default",
"apiKey": "sk_test_abc123",
"allowFrom": ["user-1", "user-2"],
"dmPolicy": "allowlist",
"capabilities": {
"text": true,
"images": false,
"files": false
}
}
]
}
}
}
```
#### Full-Featured Config
```json5
{
"channels": {
"websocket": {
"port": 8765,
"path": "/ws",
"accounts": [
{
"accountId": "production",
"apiKey": "sk_prod_xyz789",
"allowFrom": [],
"dmPolicy": "open",
"capabilities": {
"text": true,
"images": true,
"files": true,
"voice": true,
"reactions": true,
"threads": true,
"typing": true
},
"metadata": {
"serverName": "Production WS",
"region": "eu-west"
}
}
]
}
}
}
```
### 7. Testing Requirements
- Unit tests for account resolution
- Unit tests for capability validation
- Integration tests for WebSocket handshake
- Integration tests for message send/receive
- Tests for authentication failures
- Tests for DM policy enforcement
### 8. Documentation
Create `/channels/websocket.md` in OpenClaw docs with:
- Setup guide
- Configuration reference
- Message protocol specification
- Client implementation examples (JavaScript/Python)
- Security best practices
---
## Implementation Checklist
- [ ] Create package structure
- [ ] Write manifest with config schema
- [ ] Implement WebSocket server
- [ ] Implement authentication (API keys)
- [ ] Implement message protocol
- [ ] Implement capabilities advertisement
- [ ] Implement DM security
- [ ] Implement pairing flow
- [ ] Implement threading
- [ ] Implement outbound messaging
- [ ] Write tests
- [ ] Write documentation
- [ ] Test with sample client
---
## Notes for Developer
- Follow patterns from `extensions/discord` and `extensions/telegram`
- Use `createChatChannelPlugin` builder for cleaner code
- Keep WebSocket protocol simple and extensible
- Log all connection events for debugging
- Handle reconnection gracefully
- Support multiple concurrent connections per account
- Consider rate limiting per connection