@contextableai/clawg-ui 0.1.1 → 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 +16 -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 +133 -30
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
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
|
+
|
|
3
19
|
## 0.1.1 (2026-02-03)
|
|
4
20
|
|
|
5
21
|
### 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
|
|
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,
|