@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 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 contextable/clawg-ui
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 contextable/clawg-ui
16
+ openclaw plugins install @contextableai/clawg-ui
17
17
  ```
18
18
 
19
- Then restart the gateway. The plugin auto-registers the `/v1/agui` endpoint and the `agui` channel.
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/agui`. When an AG-UI client POSTs a `RunAgentInput` payload, the plugin:
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 the gateway bearer token
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/agui (RunAgentInput) |
34
+ | POST /v1/clawg-ui (RunAgentInput) |
35
35
  |------------------------------------->|
36
- | | Auth (bearer token)
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 gateway auth token configured (`OPENCLAW_GATEWAY_TOKEN` env var or `gateway.auth.token` in config)
63
+ - A paired device token (see [Authentication](#authentication))
64
64
 
65
65
  ### curl
66
66
 
67
67
  ```bash
68
- curl -N -X POST http://localhost:18789/v1/agui \
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 $OPENCLAW_GATEWAY_TOKEN" \
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/agui",
91
+ url: "http://localhost:18789/v1/clawg-ui",
88
92
  headers: {
89
- Authorization: `Bearer ${process.env.OPENCLAW_GATEWAY_TOKEN}`,
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 AG-UI" },
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/agui"
121
+ runtimeUrl="http://localhost:18789/v1/clawg-ui"
115
122
  headers={{
116
- Authorization: `Bearer ${process.env.OPENCLAW_GATEWAY_TOKEN}`,
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
- The endpoint uses the same bearer token as the OpenClaw gateway. Set it via:
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
- - Environment variable: `OPENCLAW_GATEWAY_TOKEN`
168
- - Config file: `gateway.auth.token`
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
- Pass it in the `Authorization` header:
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
- Authorization: Bearer <token>
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/agui \
182
- -H "Authorization: Bearer $OPENCLAW_GATEWAY_TOKEN" \
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 | Unauthorized (missing or invalid token) |
195
- | 405 | Method not allowed (only POST accepted) |
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/contextable/clawg-ui
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: "AG-UI",
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.1.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": "AG-UI",
39
- "docsPath": "/channels/agui",
40
- "docsLabel": "agui",
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
  };
@@ -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: "Unauthorized", type: "unauthorized" } });
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
- // Token-based auth check against gateway config
183
+ // Gateway secret resolution
138
184
  // ---------------------------------------------------------------------------
139
185
 
140
- function authenticateRequest(
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 configuredToken =
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 configuredToken !== "string" || !configuredToken) {
155
- return false;
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
- 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,84 @@ export function createAguiHttpHandler(api: OpenClawPluginApi) {
182
212
  return;
183
213
  }
184
214
 
185
- // Auth
186
- if (!authenticateRequest(req, api)) {
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:${threadId}`,
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: `clawg-ui-${threadId}`,
428
+ SenderId: deviceId,
326
429
  Provider: "clawg-ui" as const,
327
430
  Surface: "clawg-ui" as const,
328
431
  MessageSid: runId,