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:
root
2026-03-31 21:41:26 +02:00
parent 8dd07e791c
commit 1421f9d645
4 changed files with 355 additions and 21 deletions

View File

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