Add capabilities negotiation flow
Changes: - Server sends 'request_capabilities' after handshake - Client responds with 'capabilities_response' containing supported features - Server computes intersection of account + client capabilities - Effective capabilities used for message validation - Client metadata stored per-connection - Updated tests for capabilities negotiation - Updated CLIENT_SPEC.md documentation Protocol messages added: - request_capabilities (server → client) - capabilities_response (client → server) ConnectedClient fields added: - clientCapabilities?: ChannelCapabilities - clientMetadata?: Record<string, unknown> - capabilitiesReady: boolean New methods: - getEffectiveCapabilities(client) - returns intersection - clientSupportsCapability(client, capability) - check support
This commit is contained in:
@@ -30,6 +30,25 @@ ws.on('message', (data) => {
|
||||
case 'handshake':
|
||||
console.log('Server capabilities:', message.capabilities);
|
||||
break;
|
||||
case 'request_capabilities':
|
||||
// Send client capabilities in response
|
||||
ws.send(JSON.stringify({
|
||||
type: 'capabilities_response',
|
||||
capabilities: {
|
||||
text: true,
|
||||
images: true,
|
||||
files: false,
|
||||
voice: false,
|
||||
reactions: false,
|
||||
threads: true,
|
||||
typing: true
|
||||
},
|
||||
metadata: {
|
||||
clientName: 'My App',
|
||||
version: '1.0.0'
|
||||
}
|
||||
}));
|
||||
break;
|
||||
case 'message':
|
||||
console.log('Bot says:', message.content.text);
|
||||
break;
|
||||
@@ -63,7 +82,25 @@ def on_message(ws, message):
|
||||
print(f"Received: {data}")
|
||||
|
||||
if data['type'] == 'handshake':
|
||||
print(f"Capabilities: {data['capabilities']}")
|
||||
print(f"Server capabilities: {data['capabilities']}")
|
||||
elif data['type'] == 'request_capabilities':
|
||||
# Send client capabilities in response
|
||||
ws.send(json.dumps({
|
||||
"type": "capabilities_response",
|
||||
"capabilities": {
|
||||
"text": True,
|
||||
"images": True,
|
||||
"files": False,
|
||||
"voice": False,
|
||||
"reactions": False,
|
||||
"threads": True,
|
||||
"typing": True
|
||||
},
|
||||
"metadata": {
|
||||
"clientName": "My Python App",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
}))
|
||||
elif data['type'] == 'message':
|
||||
print(f"Bot: {data['content']['text']}")
|
||||
|
||||
@@ -151,12 +188,12 @@ Sent once when connection is established.
|
||||
"accountId": "default",
|
||||
"capabilities": {
|
||||
"text": true,
|
||||
"images": false,
|
||||
"files": false,
|
||||
"images": true,
|
||||
"files": true,
|
||||
"voice": false,
|
||||
"reactions": false,
|
||||
"threads": false,
|
||||
"typing": false
|
||||
"threads": true,
|
||||
"typing": true
|
||||
},
|
||||
"metadata": {
|
||||
"serverName": "Production",
|
||||
@@ -167,8 +204,10 @@ Sent once when connection is established.
|
||||
```
|
||||
|
||||
**Fields:**
|
||||
- `capabilities` - What this account supports
|
||||
- `metadata` - Custom metadata from config
|
||||
- `capabilities` - **Maximum capabilities allowed by server** (account config)
|
||||
- `metadata` - Custom metadata from server config
|
||||
|
||||
**Note:** These are the **maximum** capabilities the server allows. The client must still report its own capabilities.
|
||||
|
||||
---
|
||||
|
||||
@@ -274,8 +313,60 @@ Used for DM pairing flow.
|
||||
|
||||
---
|
||||
|
||||
#### 8. Capabilities Request
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "request_capabilities"
|
||||
}
|
||||
```
|
||||
|
||||
Sent by server **after handshake** to request client's capabilities.
|
||||
|
||||
**Client must respond with `capabilities_response`** (see below).
|
||||
|
||||
---
|
||||
|
||||
### Client → Server
|
||||
|
||||
#### 0. Capabilities Response (required)
|
||||
|
||||
**Sent in response to `request_capabilities` from server.**
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "capabilities_response",
|
||||
"capabilities": {
|
||||
"text": true,
|
||||
"images": true,
|
||||
"files": false,
|
||||
"voice": true,
|
||||
"reactions": false,
|
||||
"threads": true,
|
||||
"typing": true
|
||||
},
|
||||
"metadata": {
|
||||
"clientName": "Telegram Bot",
|
||||
"version": "1.0.0",
|
||||
"platform": "telegram"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Important:** Client **must** send this message after receiving `request_capabilities` from server.
|
||||
|
||||
**Fields:**
|
||||
- `capabilities` - What **this client** supports (true = supported, false = not supported)
|
||||
- `metadata` - Optional client metadata (name, version, platform, etc.)
|
||||
|
||||
**Effective Capabilities:**
|
||||
The server uses the **intersection** of server capabilities and client capabilities:
|
||||
- Server allows: `{ images: true, voice: false }`
|
||||
- Client supports: `{ images: true, voice: true }`
|
||||
- **Effective:** `{ images: true, voice: false }` (most restrictive wins)
|
||||
|
||||
---
|
||||
|
||||
#### 1. Text Message
|
||||
|
||||
```json
|
||||
@@ -471,6 +562,10 @@ class OpenClawClient {
|
||||
this.handleMessage(message);
|
||||
});
|
||||
|
||||
this.ws.on('open', () => {
|
||||
console.log('Connected to OpenClaw');
|
||||
});
|
||||
|
||||
this.ws.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
@@ -489,6 +584,27 @@ class OpenClawClient {
|
||||
console.log('Server capabilities:', message.capabilities);
|
||||
break;
|
||||
|
||||
case 'request_capabilities':
|
||||
// Send client capabilities
|
||||
this.ws.send(JSON.stringify({
|
||||
type: 'capabilities_response',
|
||||
capabilities: {
|
||||
text: true,
|
||||
images: this.supportsImages(),
|
||||
files: this.supportsFiles(),
|
||||
voice: this.supportsVoice(),
|
||||
reactions: false,
|
||||
threads: true,
|
||||
typing: true
|
||||
},
|
||||
metadata: {
|
||||
clientName: 'OpenClaw WebSocket Client',
|
||||
version: '1.0.0'
|
||||
}
|
||||
}));
|
||||
console.log('Sent client capabilities');
|
||||
break;
|
||||
|
||||
case 'message':
|
||||
// Bot sent a message
|
||||
this.messageHandlers.forEach(h => h(message));
|
||||
@@ -509,6 +625,11 @@ class OpenClawClient {
|
||||
}
|
||||
}
|
||||
|
||||
// Override these methods based on your client's capabilities
|
||||
supportsImages() { return true; }
|
||||
supportsFiles() { return false; }
|
||||
supportsVoice() { return false; }
|
||||
|
||||
onMessage(handler) {
|
||||
this.messageHandlers.push(handler);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user