@contextableai/clawg-ui 0.2.8 → 0.2.9
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 +24 -2
- package/index.ts +122 -76
- package/package.json +1 -1
- package/src/http-handler.ts +9 -1
- package/test-device-setup.md +0 -28
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.9 (2026-03-06)
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- Add `auth: "plugin"` to `registerHttpRoute` call — required by OpenClaw 2026.3.2; omitting it silently dropped the `/v1/clawg-ui` route, causing 404s
|
|
7
|
+
- Pass `{ channel, accountId }` object to `readAllowFromStore` instead of a bare string — fixes 403 responses for approved devices after the pairing API changed in 2026.3.2
|
|
8
|
+
- Add `pairing_code` and `bearer_token` at the root of the 403 pairing response alongside the existing nested `error.pairing` fields — restores compatibility with Kotlin `ClawgUIPairingResponse` clients expecting flat fields
|
|
9
|
+
- Add diagnostic `console.log` for 400 responses to aid debugging of malformed requests
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- README event table was missing `TOOL_CALL_ARGS` and `TOOL_CALL_RESULT`; `tools` field incorrectly said "reserved for future use"
|
|
13
|
+
- Integration tests used the gateway token directly instead of an HMAC-signed device token, causing 401s against v0.2.0+ servers
|
|
14
|
+
- "Missing auth" integration test expected 401 instead of 403 (pairing initiation)
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- "Tool call events" documentation section explaining client vs server tool flows and diagnostic tips
|
|
18
|
+
- Unit tests for `handleBeforeToolCall` and `handleToolResultPersist` hook handlers (`src/tool-hooks.test.ts`)
|
|
19
|
+
- Extracted hook handlers from `index.ts` into exported named functions for testability (no behavioral change)
|
|
20
|
+
- Integration tests now accept `CLAWG_UI_DEVICE_TOKEN` or auto-generate one from `OPENCLAW_GATEWAY_TOKEN` + `CLAWG_UI_DEVICE_ID`
|
|
21
|
+
|
|
22
|
+
## Unreleased
|
|
23
|
+
|
|
3
24
|
## 0.2.8 (2026-02-26)
|
|
4
25
|
|
|
5
26
|
### Fixed
|
package/README.md
CHANGED
|
@@ -47,6 +47,10 @@ AG-UI Client OpenClaw Gateway
|
|
|
47
47
|
|<-------------------------------------|
|
|
48
48
|
| SSE: TOOL_CALL_START |
|
|
49
49
|
|<-------------------------------------| (if agent uses tools)
|
|
50
|
+
| SSE: TOOL_CALL_ARGS |
|
|
51
|
+
|<-------------------------------------|
|
|
52
|
+
| SSE: TOOL_CALL_RESULT |
|
|
53
|
+
|<-------------------------------------| (server tools only)
|
|
50
54
|
| SSE: TOOL_CALL_END |
|
|
51
55
|
|<-------------------------------------|
|
|
52
56
|
| SSE: TEXT_MESSAGE_END |
|
|
@@ -138,7 +142,7 @@ The endpoint accepts a POST with a JSON body matching the AG-UI `RunAgentInput`
|
|
|
138
142
|
| `threadId` | string | no | Conversation thread ID. Auto-generated if omitted. |
|
|
139
143
|
| `runId` | string | no | Unique run ID. Auto-generated if omitted. |
|
|
140
144
|
| `messages` | Message[] | yes | Array of messages. At least one `user` message required. |
|
|
141
|
-
| `tools` | Tool[] | no | Client-side tool definitions
|
|
145
|
+
| `tools` | Tool[] | no | Client-side tool definitions. The agent can invoke these; see [Tool call events](#tool-call-events). |
|
|
142
146
|
| `state` | object | no | Client state (reserved for future use). |
|
|
143
147
|
|
|
144
148
|
### Message format
|
|
@@ -163,10 +167,28 @@ The response is an SSE stream. Each event is a `data:` line containing a JSON ob
|
|
|
163
167
|
| `TEXT_MESSAGE_CONTENT` | Each streamed text delta |
|
|
164
168
|
| `TEXT_MESSAGE_END` | After last text chunk |
|
|
165
169
|
| `TOOL_CALL_START` | Agent invokes a tool |
|
|
166
|
-
| `
|
|
170
|
+
| `TOOL_CALL_ARGS` | Tool call arguments (JSON delta) |
|
|
171
|
+
| `TOOL_CALL_RESULT` | Server-side tool execution result |
|
|
172
|
+
| `TOOL_CALL_END` | Tool call complete |
|
|
167
173
|
| `RUN_FINISHED` | Agent run complete |
|
|
168
174
|
| `RUN_ERROR` | On failure |
|
|
169
175
|
|
|
176
|
+
### Tool call events
|
|
177
|
+
|
|
178
|
+
Tool call events (`TOOL_CALL_START`, `TOOL_CALL_ARGS`, `TOOL_CALL_RESULT`, `TOOL_CALL_END`) are emitted when the OpenClaw agent invokes a tool during its run. They are emitted via OpenClaw lifecycle hooks (`before_tool_call` and `tool_result_persist`) in `index.ts`.
|
|
179
|
+
|
|
180
|
+
**When do tool events appear?**
|
|
181
|
+
|
|
182
|
+
- The agent must have tools available (server-side tools on the agent, or client-side tools passed via the `tools` field)
|
|
183
|
+
- The agent's LLM must decide to call a tool based on the conversation
|
|
184
|
+
|
|
185
|
+
**Client tools vs server tools:**
|
|
186
|
+
|
|
187
|
+
- **Client tools** (passed via `tools` in the request): the stream emits `TOOL_CALL_START` → `TOOL_CALL_ARGS` → `TOOL_CALL_END`, then the run finishes. The client executes the tool locally and starts a new run with the result as a `tool` message.
|
|
188
|
+
- **Server tools** (registered on the OpenClaw agent): the stream emits `TOOL_CALL_START` → `TOOL_CALL_ARGS` → `TOOL_CALL_RESULT` → `TOOL_CALL_END`. The agent continues processing in the same or a subsequent run.
|
|
189
|
+
|
|
190
|
+
> **Tip:** To confirm tool calls are being triggered, check the gateway logs for `[clawg-ui] before_tool_call:` entries.
|
|
191
|
+
|
|
170
192
|
## Authentication
|
|
171
193
|
|
|
172
194
|
clawg-ui uses **device pairing** to authenticate clients. This provides secure, per-device access control without exposing the gateway's master token.
|
package/index.ts
CHANGED
|
@@ -16,6 +16,125 @@ import {
|
|
|
16
16
|
setToolFiredInRun,
|
|
17
17
|
} from "./src/tool-store.js";
|
|
18
18
|
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Hook handlers — exported for testability
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
export interface BeforeToolCallEvent {
|
|
24
|
+
toolName: string;
|
|
25
|
+
params?: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ToolCallContext {
|
|
29
|
+
sessionKey?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Handles the `before_tool_call` OpenClaw hook.
|
|
34
|
+
* Emits TOOL_CALL_START + TOOL_CALL_ARGS (and TOOL_CALL_END for client tools).
|
|
35
|
+
*/
|
|
36
|
+
export function handleBeforeToolCall(
|
|
37
|
+
event: BeforeToolCallEvent,
|
|
38
|
+
ctx: ToolCallContext,
|
|
39
|
+
): void {
|
|
40
|
+
const sk = ctx.sessionKey;
|
|
41
|
+
console.log(
|
|
42
|
+
`[clawg-ui] before_tool_call: tool=${event.toolName}, sessionKey=${sk ?? "none"}, hasParams=${!!(event.params && Object.keys(event.params).length > 0)}, params=${JSON.stringify(event.params ?? {})}`,
|
|
43
|
+
);
|
|
44
|
+
if (!sk) {
|
|
45
|
+
console.log(`[clawg-ui] before_tool_call: skipping, no sessionKey`);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const writer = getWriter(sk);
|
|
49
|
+
if (!writer) {
|
|
50
|
+
console.log(
|
|
51
|
+
`[clawg-ui] before_tool_call: skipping, no writer for sessionKey=${sk}`,
|
|
52
|
+
);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const toolCallId = `tool-${randomUUID()}`;
|
|
56
|
+
console.log(
|
|
57
|
+
`[clawg-ui] before_tool_call: emitting TOOL_CALL_START, toolCallId=${toolCallId}`,
|
|
58
|
+
);
|
|
59
|
+
writer({
|
|
60
|
+
type: EventType.TOOL_CALL_START,
|
|
61
|
+
toolCallId,
|
|
62
|
+
toolCallName: event.toolName,
|
|
63
|
+
});
|
|
64
|
+
setToolFiredInRun(sk);
|
|
65
|
+
if (event.params && Object.keys(event.params).length > 0) {
|
|
66
|
+
console.log(
|
|
67
|
+
`[clawg-ui] before_tool_call: emitting TOOL_CALL_ARGS, params=${JSON.stringify(event.params)}`,
|
|
68
|
+
);
|
|
69
|
+
writer({
|
|
70
|
+
type: EventType.TOOL_CALL_ARGS,
|
|
71
|
+
toolCallId,
|
|
72
|
+
delta: JSON.stringify(event.params),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (isClientTool(sk, event.toolName)) {
|
|
77
|
+
// Client tool: emit TOOL_CALL_END now. The run will finish and the
|
|
78
|
+
// client initiates a new run with the tool result.
|
|
79
|
+
console.log(
|
|
80
|
+
`[clawg-ui] before_tool_call: client tool detected, emitting TOOL_CALL_END immediately`,
|
|
81
|
+
);
|
|
82
|
+
writer({
|
|
83
|
+
type: EventType.TOOL_CALL_END,
|
|
84
|
+
toolCallId,
|
|
85
|
+
});
|
|
86
|
+
setClientToolCalled(sk);
|
|
87
|
+
} else {
|
|
88
|
+
// Server tool: push ID so tool_result_persist can emit
|
|
89
|
+
// TOOL_CALL_RESULT + TOOL_CALL_END after execute() completes.
|
|
90
|
+
console.log(
|
|
91
|
+
`[clawg-ui] before_tool_call: server tool, pushing toolCallId to stack`,
|
|
92
|
+
);
|
|
93
|
+
pushToolCallId(sk, toolCallId);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Handles the `tool_result_persist` OpenClaw hook.
|
|
99
|
+
* Emits TOOL_CALL_RESULT + TOOL_CALL_END for server-side tools.
|
|
100
|
+
*/
|
|
101
|
+
export function handleToolResultPersist(
|
|
102
|
+
event: Record<string, unknown>,
|
|
103
|
+
ctx: ToolCallContext,
|
|
104
|
+
): void {
|
|
105
|
+
const sk = ctx.sessionKey;
|
|
106
|
+
console.log(
|
|
107
|
+
`[clawg-ui] tool_result_persist: sessionKey=${sk ?? "none"}, event=${JSON.stringify(event)}`,
|
|
108
|
+
);
|
|
109
|
+
if (!sk) {
|
|
110
|
+
console.log(
|
|
111
|
+
`[clawg-ui] tool_result_persist: skipping, no sessionKey`,
|
|
112
|
+
);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const writer = getWriter(sk);
|
|
116
|
+
const toolCallId = popToolCallId(sk);
|
|
117
|
+
const messageId = getMessageId(sk);
|
|
118
|
+
console.log(
|
|
119
|
+
`[clawg-ui] tool_result_persist: writer=${writer ? "present" : "missing"}, toolCallId=${toolCallId ?? "none"}, messageId=${messageId ?? "none"}`,
|
|
120
|
+
);
|
|
121
|
+
if (writer && toolCallId && messageId) {
|
|
122
|
+
console.log(
|
|
123
|
+
`[clawg-ui] tool_result_persist: emitting TOOL_CALL_RESULT and TOOL_CALL_END`,
|
|
124
|
+
);
|
|
125
|
+
writer({
|
|
126
|
+
type: EventType.TOOL_CALL_RESULT,
|
|
127
|
+
toolCallId,
|
|
128
|
+
messageId,
|
|
129
|
+
content: "",
|
|
130
|
+
});
|
|
131
|
+
writer({
|
|
132
|
+
type: EventType.TOOL_CALL_END,
|
|
133
|
+
toolCallId,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
19
138
|
const plugin = {
|
|
20
139
|
id: "clawg-ui",
|
|
21
140
|
name: "CLAWG-UI",
|
|
@@ -26,85 +145,12 @@ const plugin = {
|
|
|
26
145
|
api.registerTool(clawgUiToolFactory);
|
|
27
146
|
api.registerHttpRoute({
|
|
28
147
|
path: "/v1/clawg-ui",
|
|
148
|
+
auth: "plugin",
|
|
29
149
|
handler: createAguiHttpHandler(api),
|
|
30
150
|
});
|
|
31
151
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
// For server tools: TOOL_CALL_END is emitted later by tool_result_persist.
|
|
35
|
-
api.on("before_tool_call", (event, ctx) => {
|
|
36
|
-
const sk = ctx.sessionKey;
|
|
37
|
-
console.log(`[clawg-ui] before_tool_call: tool=${event.toolName}, sessionKey=${sk ?? "none"}, hasParams=${!!(event.params && Object.keys(event.params).length > 0)}, params=${JSON.stringify(event.params ?? {})}`);
|
|
38
|
-
if (!sk) {
|
|
39
|
-
console.log(`[clawg-ui] before_tool_call: skipping, no sessionKey`);
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
const writer = getWriter(sk);
|
|
43
|
-
if (!writer) {
|
|
44
|
-
console.log(`[clawg-ui] before_tool_call: skipping, no writer for sessionKey=${sk}`);
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
const toolCallId = `tool-${randomUUID()}`;
|
|
48
|
-
console.log(`[clawg-ui] before_tool_call: emitting TOOL_CALL_START, toolCallId=${toolCallId}`);
|
|
49
|
-
writer({
|
|
50
|
-
type: EventType.TOOL_CALL_START,
|
|
51
|
-
toolCallId,
|
|
52
|
-
toolCallName: event.toolName,
|
|
53
|
-
});
|
|
54
|
-
setToolFiredInRun(sk);
|
|
55
|
-
if (event.params && Object.keys(event.params).length > 0) {
|
|
56
|
-
console.log(`[clawg-ui] before_tool_call: emitting TOOL_CALL_ARGS, params=${JSON.stringify(event.params)}`);
|
|
57
|
-
writer({
|
|
58
|
-
type: EventType.TOOL_CALL_ARGS,
|
|
59
|
-
toolCallId,
|
|
60
|
-
delta: JSON.stringify(event.params),
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (isClientTool(sk, event.toolName)) {
|
|
65
|
-
// Client tool: emit TOOL_CALL_END now. The run will finish and the
|
|
66
|
-
// client initiates a new run with the tool result.
|
|
67
|
-
console.log(`[clawg-ui] before_tool_call: client tool detected, emitting TOOL_CALL_END immediately`);
|
|
68
|
-
writer({
|
|
69
|
-
type: EventType.TOOL_CALL_END,
|
|
70
|
-
toolCallId,
|
|
71
|
-
});
|
|
72
|
-
setClientToolCalled(sk);
|
|
73
|
-
} else {
|
|
74
|
-
// Server tool: push ID so tool_result_persist can emit
|
|
75
|
-
// TOOL_CALL_RESULT + TOOL_CALL_END after execute() completes.
|
|
76
|
-
console.log(`[clawg-ui] before_tool_call: server tool, pushing toolCallId to stack`);
|
|
77
|
-
pushToolCallId(sk, toolCallId);
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
// Emit TOOL_CALL_RESULT + TOOL_CALL_END for server-side tools only.
|
|
82
|
-
// Client tools already emitted TOOL_CALL_END in before_tool_call.
|
|
83
|
-
api.on("tool_result_persist", (event, ctx) => {
|
|
84
|
-
const sk = ctx.sessionKey;
|
|
85
|
-
console.log(`[clawg-ui] tool_result_persist: sessionKey=${sk ?? "none"}, event=${JSON.stringify(event)}`);
|
|
86
|
-
if (!sk) {
|
|
87
|
-
console.log(`[clawg-ui] tool_result_persist: skipping, no sessionKey`);
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
const writer = getWriter(sk);
|
|
91
|
-
const toolCallId = popToolCallId(sk);
|
|
92
|
-
const messageId = getMessageId(sk);
|
|
93
|
-
console.log(`[clawg-ui] tool_result_persist: writer=${writer ? "present" : "missing"}, toolCallId=${toolCallId ?? "none"}, messageId=${messageId ?? "none"}`);
|
|
94
|
-
if (writer && toolCallId && messageId) {
|
|
95
|
-
console.log(`[clawg-ui] tool_result_persist: emitting TOOL_CALL_RESULT and TOOL_CALL_END`);
|
|
96
|
-
writer({
|
|
97
|
-
type: EventType.TOOL_CALL_RESULT,
|
|
98
|
-
toolCallId,
|
|
99
|
-
messageId,
|
|
100
|
-
content: "",
|
|
101
|
-
});
|
|
102
|
-
writer({
|
|
103
|
-
type: EventType.TOOL_CALL_END,
|
|
104
|
-
toolCallId,
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
});
|
|
152
|
+
api.on("before_tool_call", handleBeforeToolCall);
|
|
153
|
+
api.on("tool_result_persist", handleToolResultPersist);
|
|
108
154
|
|
|
109
155
|
// CLI commands for device management
|
|
110
156
|
api.registerCli(
|
package/package.json
CHANGED
package/src/http-handler.ts
CHANGED
|
@@ -245,6 +245,8 @@ export function createAguiHttpHandler(api: OpenClawPluginApi) {
|
|
|
245
245
|
|
|
246
246
|
// Return pairing pending response with device token and pairing code
|
|
247
247
|
sendJson(res, 403, {
|
|
248
|
+
pairing_code: pairingCode,
|
|
249
|
+
bearer_token: deviceToken,
|
|
248
250
|
error: {
|
|
249
251
|
type: "pairing_pending",
|
|
250
252
|
message: "Device pending approval",
|
|
@@ -270,7 +272,7 @@ export function createAguiHttpHandler(api: OpenClawPluginApi) {
|
|
|
270
272
|
// Pairing check: verify device is approved
|
|
271
273
|
// ---------------------------------------------------------------------------
|
|
272
274
|
const storeAllowFrom = await runtime.channel.pairing
|
|
273
|
-
.readAllowFromStore("clawg-ui")
|
|
275
|
+
.readAllowFromStore({ channel: "clawg-ui", accountId: "default" })
|
|
274
276
|
.catch(() => []);
|
|
275
277
|
const normalizedAllowFrom = storeAllowFrom.map((e) =>
|
|
276
278
|
e.replace(/^clawg-ui:/i, "").toLowerCase(),
|
|
@@ -314,6 +316,9 @@ export function createAguiHttpHandler(api: OpenClawPluginApi) {
|
|
|
314
316
|
const hasUserMessage = messages.some((m) => m.role === "user");
|
|
315
317
|
const hasToolMessage = messages.some((m) => m.role === "tool");
|
|
316
318
|
if (!hasUserMessage && !hasToolMessage) {
|
|
319
|
+
console.log(
|
|
320
|
+
`[clawg-ui] 400: no user/tool message, roles=[${messages.map((m) => m.role).join(",")}], messageCount=${messages.length}`,
|
|
321
|
+
);
|
|
317
322
|
sendJson(res, 400, {
|
|
318
323
|
error: {
|
|
319
324
|
message: "At least one user or tool message is required in `messages`.",
|
|
@@ -326,6 +331,9 @@ export function createAguiHttpHandler(api: OpenClawPluginApi) {
|
|
|
326
331
|
// Build body from messages
|
|
327
332
|
const { body: messageBody } = buildBodyFromMessages(messages);
|
|
328
333
|
if (!messageBody.trim()) {
|
|
334
|
+
console.log(
|
|
335
|
+
`[clawg-ui] 400: empty extracted body, roles=[${messages.map((m) => m.role).join(",")}], contents=[${messages.map((m) => JSON.stringify(m.content)).join(",")}]`,
|
|
336
|
+
);
|
|
329
337
|
sendJson(res, 400, {
|
|
330
338
|
error: {
|
|
331
339
|
message: "Could not extract a prompt from `messages`.",
|
package/test-device-setup.md
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
# Integration Test Setup
|
|
2
|
-
|
|
3
|
-
To run integration tests against the local gateway:
|
|
4
|
-
|
|
5
|
-
1. Initiate device pairing:
|
|
6
|
-
```bash
|
|
7
|
-
curl -X POST http://localhost:18789/v1/clawg-ui \
|
|
8
|
-
-H "Content-Type: application/json" \
|
|
9
|
-
-d '{"messages":[{"role":"user","content":"test"}]}'
|
|
10
|
-
```
|
|
11
|
-
|
|
12
|
-
2. Copy the pairing code from the 403 response
|
|
13
|
-
|
|
14
|
-
3. Approve the device:
|
|
15
|
-
```bash
|
|
16
|
-
openclaw pairing approve clawg-ui <pairing-code>
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
4. Copy the device token from step 1's response
|
|
20
|
-
|
|
21
|
-
5. Run tests with the device token:
|
|
22
|
-
```bash
|
|
23
|
-
OPENCLAW_SERVER_URL="http://localhost" \
|
|
24
|
-
OPENCLAW_GATEWAY_TOKEN="<device-token>" \
|
|
25
|
-
npm test
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
**Note**: The OPENCLAW_GATEWAY_TOKEN env var name is misleading in the integration tests - it should be renamed to OPENCLAW_DEVICE_TOKEN in a future update.
|