@contextableai/clawg-ui 0.1.0 → 0.2.0
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 +22 -0
- package/README.md +142 -28
- package/index.ts +27 -1
- package/package.json +9 -4
- package/src/channel.ts +4 -0
- package/src/http-handler.ts +133 -30
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0 (2026-02-04)
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **Device pairing authentication** - Secure per-device access control
|
|
7
|
+
- HMAC-signed device tokens (no master token exposure)
|
|
8
|
+
- Pairing approval workflow (`openclaw pairing approve clawg-ui <code>`)
|
|
9
|
+
- New CLI command: `openclaw clawg-ui devices` - List approved devices
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- **Breaking:** Direct bearer token authentication using `OPENCLAW_GATEWAY_TOKEN` is now deprecated and no longer supported. All clients must use device pairing.
|
|
13
|
+
|
|
14
|
+
### Security
|
|
15
|
+
- Device tokens are HMAC-signed and do not expose the gateway's master secret
|
|
16
|
+
- Pending pairing requests expire after 10 minutes (max 3 per channel)
|
|
17
|
+
- Each device requires explicit approval by the gateway owner
|
|
18
|
+
|
|
19
|
+
## 0.1.1 (2026-02-03)
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- Endpoint path changed from `/v1/agui` to `/v1/clawg-ui`
|
|
23
|
+
- Package name changed to `@contextableai/clawg-ui`
|
|
24
|
+
|
|
3
25
|
## 0.1.0 (2026-02-02)
|
|
4
26
|
|
|
5
27
|
Initial release.
|
package/README.md
CHANGED
|
@@ -7,22 +7,22 @@ An [OpenClaw](https://github.com/openclaw/openclaw) channel plugin that exposes
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
npm install
|
|
10
|
+
npm install @contextableai/clawg-ui
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
Or with the OpenClaw plugin CLI:
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
openclaw plugins install
|
|
16
|
+
openclaw plugins install @contextableai/clawg-ui
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
Then restart the gateway. The plugin auto-registers the `/v1/
|
|
19
|
+
Then restart the gateway. The plugin auto-registers the `/v1/clawg-ui` endpoint and the `clawg-ui` channel.
|
|
20
20
|
|
|
21
21
|
## How it works
|
|
22
22
|
|
|
23
|
-
The plugin registers as an OpenClaw channel and adds an HTTP route at `/v1/
|
|
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/agui`
|
|
|
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
|
-
|
|
68
|
+
# Using your device token (obtained through pairing)
|
|
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/agui \
|
|
|
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
|
-
url: "http://localhost:18789/v1/
|
|
91
|
+
url: "http://localhost:18789/v1/clawg-ui",
|
|
88
92
|
headers: {
|
|
89
|
-
Authorization: `Bearer ${
|
|
93
|
+
Authorization: `Bearer ${deviceToken}`,
|
|
90
94
|
},
|
|
91
95
|
});
|
|
92
96
|
|
|
@@ -94,7 +98,7 @@ const stream = agent.run({
|
|
|
94
98
|
threadId: "thread-1",
|
|
95
99
|
runId: "run-1",
|
|
96
100
|
messages: [
|
|
97
|
-
{ role: "user", content: "Hello from
|
|
101
|
+
{ role: "user", content: "Hello from CLAWG-UI" },
|
|
98
102
|
],
|
|
99
103
|
});
|
|
100
104
|
|
|
@@ -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
|
-
runtimeUrl="http://localhost:18789/v1/
|
|
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
|
|
166
175
|
|
|
167
|
-
|
|
168
|
-
|
|
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
|
+
```
|
|
169
205
|
|
|
170
|
-
|
|
206
|
+
### Step-by-Step Setup
|
|
171
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 '{}'
|
|
172
216
|
```
|
|
173
|
-
|
|
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
|
+
}
|
|
174
232
|
```
|
|
175
233
|
|
|
234
|
+
The client must save the `token` for future requests.
|
|
235
|
+
|
|
236
|
+
#### 2. Approve the device (gateway owner)
|
|
237
|
+
|
|
238
|
+
The client shares the `pairingCode` with the gateway owner, who approves it:
|
|
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
|
|
246
|
+
```
|
|
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"}]}'
|
|
257
|
+
```
|
|
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
|
-
curl -N -X POST http://localhost:18789/v1/
|
|
182
|
-
-H "Authorization: Bearer $
|
|
294
|
+
curl -N -X POST http://localhost:18789/v1/clawg-ui \
|
|
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,18 +301,19 @@ curl -N -X POST http://localhost:18789/v1/agui \
|
|
|
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
|
|
|
199
313
|
## Development
|
|
200
314
|
|
|
201
315
|
```bash
|
|
202
|
-
git clone https://github.com/
|
|
316
|
+
git clone https://github.com/contextablemark/clawg-ui
|
|
203
317
|
cd clawg-ui
|
|
204
318
|
npm install
|
|
205
319
|
npm test
|
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";
|
|
@@ -15,7 +16,7 @@ import {
|
|
|
15
16
|
|
|
16
17
|
const plugin = {
|
|
17
18
|
id: "clawg-ui",
|
|
18
|
-
name: "
|
|
19
|
+
name: "CLAWG-UI",
|
|
19
20
|
description: "AG-UI protocol endpoint for CopilotKit and HttpAgent clients",
|
|
20
21
|
configSchema: emptyPluginConfigSchema(),
|
|
21
22
|
register(api: OpenClawPluginApi) {
|
|
@@ -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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contextableai/clawg-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "AG-UI protocol channel plugin for OpenClaw — connect CopilotKit and AG-UI clients to your OpenClaw gateway",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -35,11 +35,16 @@
|
|
|
35
35
|
],
|
|
36
36
|
"channel": {
|
|
37
37
|
"id": "clawg-ui",
|
|
38
|
-
"label": "
|
|
39
|
-
"docsPath": "/channels/
|
|
40
|
-
"docsLabel": "
|
|
38
|
+
"label": "CLAWG-UI",
|
|
39
|
+
"docsPath": "/channels/clawg-ui",
|
|
40
|
+
"docsLabel": "clawg-ui",
|
|
41
41
|
"blurb": "AG-UI protocol endpoint for CopilotKit and HttpAgent clients.",
|
|
42
42
|
"order": 90
|
|
43
|
+
},
|
|
44
|
+
"install": {
|
|
45
|
+
"npmSpec": "@contextableai/clawg-ui",
|
|
46
|
+
"localPath": "extensions/clawg-ui",
|
|
47
|
+
"defaultChoice": "npm"
|
|
43
48
|
}
|
|
44
49
|
}
|
|
45
50
|
}
|
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
|
|
156
|
-
}
|
|
157
|
-
// Constant-time comparison
|
|
158
|
-
if (token.length !== configuredToken.length) {
|
|
159
|
-
return false;
|
|
192
|
+
if (typeof secret === "string" && secret) {
|
|
193
|
+
return secret;
|
|
160
194
|
}
|
|
161
|
-
|
|
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,84 @@ 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
|
+
// Generate signed device token
|
|
244
|
+
const deviceToken = createDeviceToken(gatewaySecret, deviceId);
|
|
245
|
+
|
|
246
|
+
// Return pairing pending response with device token and pairing code
|
|
247
|
+
sendJson(res, 403, {
|
|
248
|
+
error: {
|
|
249
|
+
type: "pairing_pending",
|
|
250
|
+
message: "Device pending approval",
|
|
251
|
+
pairing: {
|
|
252
|
+
pairingCode,
|
|
253
|
+
token: deviceToken,
|
|
254
|
+
instructions: `Save this token for use as a Bearer token and ask the owner to approve: openclaw pairing approve clawg-ui ${pairingCode}`,
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Device token flow: verify HMAC signature, extract device ID
|
|
262
|
+
const extractedDeviceId = verifyDeviceToken(bearerToken, gatewaySecret);
|
|
263
|
+
if (!extractedDeviceId) {
|
|
187
264
|
sendUnauthorized(res);
|
|
188
265
|
return;
|
|
189
266
|
}
|
|
267
|
+
deviceId = extractedDeviceId;
|
|
268
|
+
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
// Pairing check: verify device is approved
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
const storeAllowFrom = await runtime.channel.pairing
|
|
273
|
+
.readAllowFromStore("clawg-ui")
|
|
274
|
+
.catch(() => []);
|
|
275
|
+
const normalizedAllowFrom = storeAllowFrom.map((e) =>
|
|
276
|
+
e.replace(/^clawg-ui:/i, "").toLowerCase(),
|
|
277
|
+
);
|
|
278
|
+
const allowed = normalizedAllowFrom.includes(deviceId.toLowerCase());
|
|
279
|
+
|
|
280
|
+
if (!allowed) {
|
|
281
|
+
sendJson(res, 403, {
|
|
282
|
+
error: {
|
|
283
|
+
type: "pairing_pending",
|
|
284
|
+
message: "Device pending approval. Ask the owner to approve using the pairing code from your initial pairing response.",
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ---------------------------------------------------------------------------
|
|
291
|
+
// Device approved - proceed with request
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
190
293
|
|
|
191
294
|
// Parse body
|
|
192
295
|
let body: unknown;
|
|
@@ -257,12 +360,12 @@ export function createAguiHttpHandler(api: OpenClawPluginApi) {
|
|
|
257
360
|
const messageId = `msg-${randomUUID()}`;
|
|
258
361
|
let messageStarted = false;
|
|
259
362
|
|
|
260
|
-
const writeEvent = (event: Record<string, unknown>) => {
|
|
363
|
+
const writeEvent = (event: { type: EventType } & Record<string, unknown>) => {
|
|
261
364
|
if (closed) {
|
|
262
365
|
return;
|
|
263
366
|
}
|
|
264
367
|
try {
|
|
265
|
-
res.write(encoder.encode(event));
|
|
368
|
+
res.write(encoder.encode(event as Parameters<typeof encoder.encode>[0]));
|
|
266
369
|
} catch {
|
|
267
370
|
// Client may have disconnected
|
|
268
371
|
closed = true;
|
|
@@ -316,13 +419,13 @@ export function createAguiHttpHandler(api: OpenClawPluginApi) {
|
|
|
316
419
|
Body: envelopedBody,
|
|
317
420
|
RawBody: messageBody,
|
|
318
421
|
CommandBody: messageBody,
|
|
319
|
-
From: `clawg-ui:${
|
|
422
|
+
From: `clawg-ui:${deviceId}`,
|
|
320
423
|
To: "clawg-ui",
|
|
321
424
|
SessionKey: sessionKey,
|
|
322
425
|
ChatType: "direct",
|
|
323
426
|
ConversationLabel: "AG-UI",
|
|
324
427
|
SenderName: "AG-UI Client",
|
|
325
|
-
SenderId:
|
|
428
|
+
SenderId: deviceId,
|
|
326
429
|
Provider: "clawg-ui" as const,
|
|
327
430
|
Surface: "clawg-ui" as const,
|
|
328
431
|
MessageSid: runId,
|