@contextableai/clawg-ui 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.md +132 -18
- package/index.ts +26 -0
- package/package.json +1 -1
- package/src/channel.ts +4 -0
- package/src/http-handler.ts +144 -30
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.1 (2026-02-05)
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- Return HTTP 429 `rate_limit` error when max pending pairing requests (3) is reached, instead of returning an empty pairing code
|
|
7
|
+
|
|
8
|
+
## 0.2.0 (2026-02-04)
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Device pairing authentication** - Secure per-device access control
|
|
12
|
+
- HMAC-signed device tokens (no master token exposure)
|
|
13
|
+
- Pairing approval workflow (`openclaw pairing approve clawg-ui <code>`)
|
|
14
|
+
- New CLI command: `openclaw clawg-ui devices` - List approved devices
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- **Breaking:** Direct bearer token authentication using `OPENCLAW_GATEWAY_TOKEN` is now deprecated and no longer supported. All clients must use device pairing.
|
|
18
|
+
|
|
19
|
+
### Security
|
|
20
|
+
- Device tokens are HMAC-signed and do not expose the gateway's master secret
|
|
21
|
+
- Pending pairing requests expire after 1 hour (max 3 per channel)
|
|
22
|
+
- Each device requires explicit approval by the gateway owner
|
|
23
|
+
|
|
3
24
|
## 0.1.1 (2026-02-03)
|
|
4
25
|
|
|
5
26
|
### Changed
|
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ Then restart the gateway. The plugin auto-registers the `/v1/clawg-ui` endpoint
|
|
|
22
22
|
|
|
23
23
|
The plugin registers as an OpenClaw channel and adds an HTTP route at `/v1/clawg-ui`. When an AG-UI client POSTs a `RunAgentInput` payload, the plugin:
|
|
24
24
|
|
|
25
|
-
1. Authenticates the request using
|
|
25
|
+
1. Authenticates the request using device pairing (see [Authentication](#authentication))
|
|
26
26
|
2. Parses the AG-UI messages into an OpenClaw inbound context
|
|
27
27
|
3. Routes to the appropriate agent via the gateway's standard routing
|
|
28
28
|
4. Dispatches the message through the reply pipeline (same path as Telegram, Teams, etc.)
|
|
@@ -31,9 +31,9 @@ The plugin registers as an OpenClaw channel and adds an HTTP route at `/v1/clawg
|
|
|
31
31
|
```
|
|
32
32
|
AG-UI Client OpenClaw Gateway
|
|
33
33
|
| |
|
|
34
|
-
| POST /v1/
|
|
34
|
+
| POST /v1/clawg-ui (RunAgentInput) |
|
|
35
35
|
|------------------------------------->|
|
|
36
|
-
| | Auth (
|
|
36
|
+
| | Auth (device token)
|
|
37
37
|
| | Route to agent
|
|
38
38
|
| | Dispatch inbound message
|
|
39
39
|
| |
|
|
@@ -60,15 +60,16 @@ AG-UI Client OpenClaw Gateway
|
|
|
60
60
|
### Prerequisites
|
|
61
61
|
|
|
62
62
|
- OpenClaw gateway running (`openclaw gateway run`)
|
|
63
|
-
- A
|
|
63
|
+
- A paired device token (see [Authentication](#authentication))
|
|
64
64
|
|
|
65
65
|
### curl
|
|
66
66
|
|
|
67
67
|
```bash
|
|
68
|
+
# Using your device token (obtained through pairing)
|
|
68
69
|
curl -N -X POST http://localhost:18789/v1/clawg-ui \
|
|
69
70
|
-H "Content-Type: application/json" \
|
|
70
71
|
-H "Accept: text/event-stream" \
|
|
71
|
-
-H "Authorization: Bearer $
|
|
72
|
+
-H "Authorization: Bearer $CLAWG_UI_DEVICE_TOKEN" \
|
|
72
73
|
-d '{
|
|
73
74
|
"threadId": "thread-1",
|
|
74
75
|
"runId": "run-1",
|
|
@@ -83,10 +84,13 @@ curl -N -X POST http://localhost:18789/v1/clawg-ui \
|
|
|
83
84
|
```typescript
|
|
84
85
|
import { HttpAgent } from "@ag-ui/client";
|
|
85
86
|
|
|
87
|
+
// Device token obtained through the pairing flow
|
|
88
|
+
const deviceToken = process.env.CLAWG_UI_DEVICE_TOKEN;
|
|
89
|
+
|
|
86
90
|
const agent = new HttpAgent({
|
|
87
91
|
url: "http://localhost:18789/v1/clawg-ui",
|
|
88
92
|
headers: {
|
|
89
|
-
Authorization: `Bearer ${
|
|
93
|
+
Authorization: `Bearer ${deviceToken}`,
|
|
90
94
|
},
|
|
91
95
|
});
|
|
92
96
|
|
|
@@ -108,12 +112,15 @@ for await (const event of stream) {
|
|
|
108
112
|
```tsx
|
|
109
113
|
import { CopilotKit } from "@copilotkit/react-core";
|
|
110
114
|
|
|
115
|
+
// Device token obtained through the pairing flow
|
|
116
|
+
const deviceToken = process.env.CLAWG_UI_DEVICE_TOKEN;
|
|
117
|
+
|
|
111
118
|
function App() {
|
|
112
119
|
return (
|
|
113
120
|
<CopilotKit
|
|
114
121
|
runtimeUrl="http://localhost:18789/v1/clawg-ui"
|
|
115
122
|
headers={{
|
|
116
|
-
Authorization: `Bearer ${
|
|
123
|
+
Authorization: `Bearer ${deviceToken}`,
|
|
117
124
|
}}
|
|
118
125
|
>
|
|
119
126
|
{/* your app */}
|
|
@@ -162,24 +169,130 @@ The response is an SSE stream. Each event is a `data:` line containing a JSON ob
|
|
|
162
169
|
|
|
163
170
|
## Authentication
|
|
164
171
|
|
|
165
|
-
|
|
172
|
+
clawg-ui uses **device pairing** to authenticate clients. This provides secure, per-device access control without exposing the gateway's master token.
|
|
173
|
+
|
|
174
|
+
### Device Pairing Flow
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
|
|
178
|
+
│ Gateway Owner │ │ OpenClaw Server │ │ AG-UI Client │
|
|
179
|
+
└────────┬────────┘ └────────┬─────────┘ └────────┬────────┘
|
|
180
|
+
│ │ │
|
|
181
|
+
│ │ 1. POST (no auth) │
|
|
182
|
+
│ │<────────────────────────│
|
|
183
|
+
│ │ │
|
|
184
|
+
│ │ 2. Return device token │
|
|
185
|
+
│ │ + pairing code │
|
|
186
|
+
│ │────────────────────────>│
|
|
187
|
+
│ │ 403 pairing_pending │
|
|
188
|
+
│ │ { pairingCode, token }
|
|
189
|
+
│ │ │
|
|
190
|
+
│ 3. Share pairing code (out of band) │
|
|
191
|
+
│<─────────────────────────────────────────────────│
|
|
192
|
+
│ │ │
|
|
193
|
+
4. Approve device │ │
|
|
194
|
+
│ openclaw pairing approve clawg-ui ABCD1234 │
|
|
195
|
+
│───────────────────────>│ │
|
|
196
|
+
│ │ │
|
|
197
|
+
│ │ 5. POST with device token
|
|
198
|
+
│ │<────────────────────────│
|
|
199
|
+
│ │ Authorization: Bearer <token>
|
|
200
|
+
│ │ │
|
|
201
|
+
│ │ 6. Success - SSE stream│
|
|
202
|
+
│ │────────────────────────>│
|
|
203
|
+
│ │ │
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Step-by-Step Setup
|
|
207
|
+
|
|
208
|
+
#### 1. Client initiates pairing
|
|
209
|
+
|
|
210
|
+
The client sends a POST request without any authorization header:
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
curl -X POST http://localhost:18789/v1/clawg-ui \
|
|
214
|
+
-H "Content-Type: application/json" \
|
|
215
|
+
-d '{}'
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Response (403):
|
|
219
|
+
|
|
220
|
+
```json
|
|
221
|
+
{
|
|
222
|
+
"error": {
|
|
223
|
+
"type": "pairing_pending",
|
|
224
|
+
"message": "Device pending approval",
|
|
225
|
+
"pairing": {
|
|
226
|
+
"pairingCode": "ABCD1234",
|
|
227
|
+
"token": "MmRlOTA0ODIt...b71d",
|
|
228
|
+
"instructions": "Save this token for use as a Bearer token and ask the owner to approve: openclaw pairing approve clawg-ui ABCD1234"
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
The client must save the `token` for future requests.
|
|
166
235
|
|
|
167
|
-
|
|
168
|
-
- Config file: `gateway.auth.token`
|
|
236
|
+
#### 2. Approve the device (gateway owner)
|
|
169
237
|
|
|
170
|
-
|
|
238
|
+
The client shares the `pairingCode` with the gateway owner, who approves it:
|
|
171
239
|
|
|
240
|
+
```bash
|
|
241
|
+
# List pending pairing requests
|
|
242
|
+
openclaw pairing list clawg-ui
|
|
243
|
+
|
|
244
|
+
# Approve the device
|
|
245
|
+
openclaw pairing approve clawg-ui ABCD1234
|
|
172
246
|
```
|
|
173
|
-
|
|
247
|
+
|
|
248
|
+
#### 3. Client uses Bearer token
|
|
249
|
+
|
|
250
|
+
Once approved, the client uses their Bearer token for all requests:
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
curl -N -X POST http://localhost:18789/v1/clawg-ui \
|
|
254
|
+
-H "Authorization: Bearer MmRlOTA0ODIt...b71d" \
|
|
255
|
+
-H "Content-Type: application/json" \
|
|
256
|
+
-d '{"messages":[{"role":"user","content":"Hello"}]}'
|
|
174
257
|
```
|
|
175
258
|
|
|
259
|
+
### CLI Commands
|
|
260
|
+
|
|
261
|
+
| Command | Description |
|
|
262
|
+
|---------|-------------|
|
|
263
|
+
| `openclaw clawg-ui devices` | List approved devices |
|
|
264
|
+
| `openclaw pairing list clawg-ui` | List pending pairing requests awaiting approval |
|
|
265
|
+
| `openclaw pairing approve clawg-ui <code>` | Approve a device by its pairing code |
|
|
266
|
+
|
|
267
|
+
### Error Responses
|
|
268
|
+
|
|
269
|
+
| Status | Type | Meaning |
|
|
270
|
+
|--------|------|---------|
|
|
271
|
+
| 401 | `unauthorized` | Invalid device token |
|
|
272
|
+
| 403 | `pairing_pending` | No auth (initiates pairing) or valid token but device not yet approved |
|
|
273
|
+
|
|
274
|
+
### Deprecated: Direct Bearer Token
|
|
275
|
+
|
|
276
|
+
> **Deprecated:** Previous versions (0.1.x) allowed using the gateway's master token (`OPENCLAW_GATEWAY_TOKEN`) directly. This approach is **no longer supported**. All clients must now use device pairing.
|
|
277
|
+
>
|
|
278
|
+
> Old (deprecated):
|
|
279
|
+
> ```
|
|
280
|
+
> Authorization: Bearer $OPENCLAW_GATEWAY_TOKEN
|
|
281
|
+
> ```
|
|
282
|
+
>
|
|
283
|
+
> New (required):
|
|
284
|
+
> ```
|
|
285
|
+
> POST without Authorization header # Initiates pairing
|
|
286
|
+
> Authorization: Bearer <device-token> # After approval
|
|
287
|
+
> ```
|
|
288
|
+
|
|
176
289
|
## Agent routing
|
|
177
290
|
|
|
178
291
|
The plugin uses OpenClaw's standard agent routing. By default, messages route to the `main` agent. To target a specific agent, set the `X-OpenClaw-Agent-Id` header:
|
|
179
292
|
|
|
180
293
|
```bash
|
|
181
294
|
curl -N -X POST http://localhost:18789/v1/clawg-ui \
|
|
182
|
-
-H "Authorization: Bearer $
|
|
295
|
+
-H "Authorization: Bearer $CLAWG_UI_DEVICE_TOKEN" \
|
|
183
296
|
-H "X-OpenClaw-Agent-Id: my-agent" \
|
|
184
297
|
-d '{"messages":[{"role":"user","content":"Hello"}]}'
|
|
185
298
|
```
|
|
@@ -188,11 +301,12 @@ curl -N -X POST http://localhost:18789/v1/clawg-ui \
|
|
|
188
301
|
|
|
189
302
|
Non-streaming errors return JSON:
|
|
190
303
|
|
|
191
|
-
| Status | Meaning |
|
|
192
|
-
|
|
193
|
-
| 400 | Invalid request (missing messages, bad JSON) |
|
|
194
|
-
| 401 |
|
|
195
|
-
|
|
|
304
|
+
| Status | Type | Meaning |
|
|
305
|
+
|---|---|---|
|
|
306
|
+
| 400 | `invalid_request_error` | Invalid request (missing messages, bad JSON) |
|
|
307
|
+
| 401 | `unauthorized` | Invalid device token |
|
|
308
|
+
| 403 | `pairing_pending` | No auth header (initiates pairing) or valid token but device not yet approved |
|
|
309
|
+
| 405 | — | Method not allowed (only POST accepted) |
|
|
196
310
|
|
|
197
311
|
Streaming errors emit a `RUN_ERROR` event and close the connection.
|
|
198
312
|
|
package/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
+
import type { Command } from "commander";
|
|
2
3
|
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
3
4
|
import { randomUUID } from "node:crypto";
|
|
4
5
|
import { EventType } from "@ag-ui/core";
|
|
@@ -82,6 +83,31 @@ const plugin = {
|
|
|
82
83
|
});
|
|
83
84
|
}
|
|
84
85
|
});
|
|
86
|
+
|
|
87
|
+
// CLI commands for device management
|
|
88
|
+
api.registerCli(
|
|
89
|
+
({ program }: { program: Command }) => {
|
|
90
|
+
const clawgUi = program
|
|
91
|
+
.command("clawg-ui")
|
|
92
|
+
.description("CLAWG-UI (AG-UI) channel commands");
|
|
93
|
+
|
|
94
|
+
clawgUi
|
|
95
|
+
.command("devices")
|
|
96
|
+
.description("List approved devices")
|
|
97
|
+
.action(async () => {
|
|
98
|
+
const devices = await api.runtime.channel.pairing.readAllowFromStore("clawg-ui");
|
|
99
|
+
if (devices.length === 0) {
|
|
100
|
+
console.log("No approved devices.");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
console.log("Approved devices:");
|
|
104
|
+
for (const deviceId of devices) {
|
|
105
|
+
console.log(` ${deviceId}`);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
},
|
|
109
|
+
{ commands: ["clawg-ui"] },
|
|
110
|
+
);
|
|
85
111
|
},
|
|
86
112
|
};
|
|
87
113
|
|
package/package.json
CHANGED
package/src/channel.ts
CHANGED
|
@@ -30,4 +30,8 @@ export const aguiChannelPlugin: ChannelPlugin<ResolvedAguiAccount> = {
|
|
|
30
30
|
}),
|
|
31
31
|
defaultAccountId: () => "default",
|
|
32
32
|
},
|
|
33
|
+
pairing: {
|
|
34
|
+
idLabel: "clawgUiDeviceId",
|
|
35
|
+
normalizeAllowEntry: (entry: string) => entry.replace(/^clawg-ui:/i, "").toLowerCase(),
|
|
36
|
+
},
|
|
33
37
|
};
|
package/src/http-handler.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { randomUUID, createHmac, timingSafeEqual } from "node:crypto";
|
|
3
3
|
import { EventType } from "@ag-ui/core";
|
|
4
4
|
import type { RunAgentInput, Message } from "@ag-ui/core";
|
|
5
5
|
import { EventEncoder } from "@ag-ui/encoder";
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
clearClientToolCalled,
|
|
14
14
|
clearClientToolNames,
|
|
15
15
|
} from "./tool-store.js";
|
|
16
|
+
import { aguiChannelPlugin } from "./channel.js";
|
|
16
17
|
|
|
17
18
|
// ---------------------------------------------------------------------------
|
|
18
19
|
// Lightweight HTTP helpers (no internal imports needed)
|
|
@@ -32,7 +33,7 @@ function sendMethodNotAllowed(res: ServerResponse) {
|
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
function sendUnauthorized(res: ServerResponse) {
|
|
35
|
-
sendJson(res, 401, { error: { message: "
|
|
36
|
+
sendJson(res, 401, { error: { message: "Authentication required", type: "unauthorized" } });
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
function readJsonBody(req: IncomingMessage, maxBytes: number): Promise<unknown> {
|
|
@@ -67,6 +68,51 @@ function getBearerToken(req: IncomingMessage): string | undefined {
|
|
|
67
68
|
return raw.slice(7).trim() || undefined;
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// HMAC-signed device token utilities
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
function createDeviceToken(secret: string, deviceId: string): string {
|
|
76
|
+
const encodedId = Buffer.from(deviceId).toString("base64url");
|
|
77
|
+
const signature = createHmac("sha256", secret).update(deviceId).digest("hex").slice(0, 32);
|
|
78
|
+
return `${encodedId}.${signature}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function verifyDeviceToken(token: string, secret: string): string | null {
|
|
82
|
+
const dotIndex = token.indexOf(".");
|
|
83
|
+
if (dotIndex <= 0 || dotIndex >= token.length - 1) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const encodedId = token.slice(0, dotIndex);
|
|
88
|
+
const providedSig = token.slice(dotIndex + 1);
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const deviceId = Buffer.from(encodedId, "base64url").toString("utf-8");
|
|
92
|
+
|
|
93
|
+
// Validate it looks like a UUID
|
|
94
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(deviceId)) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const expectedSig = createHmac("sha256", secret).update(deviceId).digest("hex").slice(0, 32);
|
|
99
|
+
|
|
100
|
+
// Constant-time comparison
|
|
101
|
+
if (providedSig.length !== expectedSig.length) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
const providedBuf = Buffer.from(providedSig);
|
|
105
|
+
const expectedBuf = Buffer.from(expectedSig);
|
|
106
|
+
if (!timingSafeEqual(providedBuf, expectedBuf)) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return deviceId;
|
|
111
|
+
} catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
70
116
|
// ---------------------------------------------------------------------------
|
|
71
117
|
// Extract text from AG-UI messages
|
|
72
118
|
// ---------------------------------------------------------------------------
|
|
@@ -134,35 +180,19 @@ function buildBodyFromMessages(messages: Message[]): {
|
|
|
134
180
|
}
|
|
135
181
|
|
|
136
182
|
// ---------------------------------------------------------------------------
|
|
137
|
-
//
|
|
183
|
+
// Gateway secret resolution
|
|
138
184
|
// ---------------------------------------------------------------------------
|
|
139
185
|
|
|
140
|
-
function
|
|
141
|
-
req: IncomingMessage,
|
|
142
|
-
api: OpenClawPluginApi,
|
|
143
|
-
): boolean {
|
|
144
|
-
const token = getBearerToken(req);
|
|
145
|
-
if (!token) {
|
|
146
|
-
return false;
|
|
147
|
-
}
|
|
148
|
-
// Read the configured gateway token from config
|
|
186
|
+
function getGatewaySecret(api: OpenClawPluginApi): string | null {
|
|
149
187
|
const gatewayAuth = api.config.gateway?.auth;
|
|
150
|
-
const
|
|
188
|
+
const secret =
|
|
151
189
|
(gatewayAuth as Record<string, unknown> | undefined)?.token ??
|
|
152
190
|
process.env.OPENCLAW_GATEWAY_TOKEN ??
|
|
153
191
|
process.env.CLAWDBOT_GATEWAY_TOKEN;
|
|
154
|
-
if (typeof
|
|
155
|
-
return
|
|
192
|
+
if (typeof secret === "string" && secret) {
|
|
193
|
+
return secret;
|
|
156
194
|
}
|
|
157
|
-
|
|
158
|
-
if (token.length !== configuredToken.length) {
|
|
159
|
-
return false;
|
|
160
|
-
}
|
|
161
|
-
let mismatch = 0;
|
|
162
|
-
for (let i = 0; i < token.length; i++) {
|
|
163
|
-
mismatch |= token.charCodeAt(i) ^ configuredToken.charCodeAt(i);
|
|
164
|
-
}
|
|
165
|
-
return mismatch === 0;
|
|
195
|
+
return null;
|
|
166
196
|
}
|
|
167
197
|
|
|
168
198
|
// ---------------------------------------------------------------------------
|
|
@@ -182,11 +212,95 @@ export function createAguiHttpHandler(api: OpenClawPluginApi) {
|
|
|
182
212
|
return;
|
|
183
213
|
}
|
|
184
214
|
|
|
185
|
-
//
|
|
186
|
-
|
|
215
|
+
// Get gateway secret for HMAC operations
|
|
216
|
+
const gatewaySecret = getGatewaySecret(api);
|
|
217
|
+
if (!gatewaySecret) {
|
|
218
|
+
sendJson(res, 500, {
|
|
219
|
+
error: { message: "Gateway not configured", type: "server_error" },
|
|
220
|
+
});
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
// Authentication: No auth (pairing initiation) or Device token
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
let deviceId: string;
|
|
228
|
+
|
|
229
|
+
const bearerToken = getBearerToken(req);
|
|
230
|
+
|
|
231
|
+
if (!bearerToken) {
|
|
232
|
+
// No auth header: initiate pairing
|
|
233
|
+
// Generate new device ID
|
|
234
|
+
deviceId = randomUUID();
|
|
235
|
+
|
|
236
|
+
// Add to pending via OpenClaw pairing API - returns a pairing code for approval
|
|
237
|
+
const { code: pairingCode } = await runtime.channel.pairing.upsertPairingRequest({
|
|
238
|
+
channel: "clawg-ui",
|
|
239
|
+
id: deviceId,
|
|
240
|
+
pairingAdapter: aguiChannelPlugin.pairing,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Rate limit reached - max pending requests exceeded
|
|
244
|
+
if (!pairingCode) {
|
|
245
|
+
sendJson(res, 429, {
|
|
246
|
+
error: {
|
|
247
|
+
type: "rate_limit",
|
|
248
|
+
message: "Too many pending pairing requests. Please wait for existing requests to expire (10 minutes) or ask the owner to approve/reject them.",
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Generate signed device token
|
|
255
|
+
const deviceToken = createDeviceToken(gatewaySecret, deviceId);
|
|
256
|
+
|
|
257
|
+
// Return pairing pending response with device token and pairing code
|
|
258
|
+
sendJson(res, 403, {
|
|
259
|
+
error: {
|
|
260
|
+
type: "pairing_pending",
|
|
261
|
+
message: "Device pending approval",
|
|
262
|
+
pairing: {
|
|
263
|
+
pairingCode,
|
|
264
|
+
token: deviceToken,
|
|
265
|
+
instructions: `Save this token for use as a Bearer token and ask the owner to approve: openclaw pairing approve clawg-ui ${pairingCode}`,
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Device token flow: verify HMAC signature, extract device ID
|
|
273
|
+
const extractedDeviceId = verifyDeviceToken(bearerToken, gatewaySecret);
|
|
274
|
+
if (!extractedDeviceId) {
|
|
187
275
|
sendUnauthorized(res);
|
|
188
276
|
return;
|
|
189
277
|
}
|
|
278
|
+
deviceId = extractedDeviceId;
|
|
279
|
+
|
|
280
|
+
// ---------------------------------------------------------------------------
|
|
281
|
+
// Pairing check: verify device is approved
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
const storeAllowFrom = await runtime.channel.pairing
|
|
284
|
+
.readAllowFromStore("clawg-ui")
|
|
285
|
+
.catch(() => []);
|
|
286
|
+
const normalizedAllowFrom = storeAllowFrom.map((e) =>
|
|
287
|
+
e.replace(/^clawg-ui:/i, "").toLowerCase(),
|
|
288
|
+
);
|
|
289
|
+
const allowed = normalizedAllowFrom.includes(deviceId.toLowerCase());
|
|
290
|
+
|
|
291
|
+
if (!allowed) {
|
|
292
|
+
sendJson(res, 403, {
|
|
293
|
+
error: {
|
|
294
|
+
type: "pairing_pending",
|
|
295
|
+
message: "Device pending approval. Ask the owner to approve using the pairing code from your initial pairing response.",
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
302
|
+
// Device approved - proceed with request
|
|
303
|
+
// ---------------------------------------------------------------------------
|
|
190
304
|
|
|
191
305
|
// Parse body
|
|
192
306
|
let body: unknown;
|
|
@@ -257,12 +371,12 @@ export function createAguiHttpHandler(api: OpenClawPluginApi) {
|
|
|
257
371
|
const messageId = `msg-${randomUUID()}`;
|
|
258
372
|
let messageStarted = false;
|
|
259
373
|
|
|
260
|
-
const writeEvent = (event: Record<string, unknown>) => {
|
|
374
|
+
const writeEvent = (event: { type: EventType } & Record<string, unknown>) => {
|
|
261
375
|
if (closed) {
|
|
262
376
|
return;
|
|
263
377
|
}
|
|
264
378
|
try {
|
|
265
|
-
res.write(encoder.encode(event));
|
|
379
|
+
res.write(encoder.encode(event as Parameters<typeof encoder.encode>[0]));
|
|
266
380
|
} catch {
|
|
267
381
|
// Client may have disconnected
|
|
268
382
|
closed = true;
|
|
@@ -316,13 +430,13 @@ export function createAguiHttpHandler(api: OpenClawPluginApi) {
|
|
|
316
430
|
Body: envelopedBody,
|
|
317
431
|
RawBody: messageBody,
|
|
318
432
|
CommandBody: messageBody,
|
|
319
|
-
From: `clawg-ui:${
|
|
433
|
+
From: `clawg-ui:${deviceId}`,
|
|
320
434
|
To: "clawg-ui",
|
|
321
435
|
SessionKey: sessionKey,
|
|
322
436
|
ChatType: "direct",
|
|
323
437
|
ConversationLabel: "AG-UI",
|
|
324
438
|
SenderName: "AG-UI Client",
|
|
325
|
-
SenderId:
|
|
439
|
+
SenderId: deviceId,
|
|
326
440
|
Provider: "clawg-ui" as const,
|
|
327
441
|
Surface: "clawg-ui" as const,
|
|
328
442
|
MessageSid: runId,
|