@contextableai/clawg-ui 0.2.7 → 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 CHANGED
@@ -1,5 +1,31 @@
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
+
24
+ ## 0.2.8 (2026-02-26)
25
+
26
+ ### Fixed
27
+ - Remove literal `process.env` from a code comment in `http-handler.ts` that was itself triggering the security scanner — the comment documenting the v0.2.5/v0.2.6 fix contained the exact pattern the scanner flags
28
+
3
29
  ## 0.2.7 (2026-02-18)
4
30
 
5
31
  ### 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 (reserved for future use). |
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
- | `TOOL_CALL_END` | Tool execution complete |
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
- // Emit TOOL_CALL_START + TOOL_CALL_ARGS from before_tool_call hook.
33
- // For client tools: also emit TOOL_CALL_END immediately (fire-and-forget).
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contextableai/clawg-ui",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
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",
@@ -189,7 +189,7 @@ function buildBodyFromMessages(messages: Message[]): {
189
189
  export function createAguiHttpHandler(api: OpenClawPluginApi) {
190
190
  const runtime: PluginRuntime = api.runtime;
191
191
 
192
- // Resolve once at init so the per-request handler never touches process.env.
192
+ // Resolve once at init so the per-request handler never touches env vars.
193
193
  const gatewaySecret = resolveGatewaySecret(api);
194
194
 
195
195
  return async function handleAguiRequest(
@@ -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`.",
@@ -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.