@contextableai/clawg-ui 0.2.8 → 0.3.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/README.md +24 -2
- package/dist/index.d.ts +27 -0
- package/dist/index.js +122 -0
- package/dist/src/channel.d.ts +8 -0
- package/dist/src/channel.js +29 -0
- package/dist/src/client-tools.d.ts +27 -0
- package/dist/src/client-tools.js +50 -0
- package/dist/src/gateway-secret.d.ts +9 -0
- package/dist/src/gateway-secret.js +17 -0
- package/dist/src/http-handler.d.ts +3 -0
- package/dist/src/http-handler.js +545 -0
- package/dist/src/tool-store.d.ts +21 -0
- package/dist/src/tool-store.js +109 -0
- package/package.json +13 -2
- package/CHANGELOG.md +0 -84
- package/clawgui.png +0 -0
- package/index.ts +0 -136
- package/src/channel.ts +0 -37
- package/src/client-tools.ts +0 -51
- package/src/gateway-secret.ts +0 -20
- package/src/http-handler.ts +0 -619
- package/src/tool-store.ts +0 -152
- package/test-device-setup.md +0 -28
- package/tsconfig.json +0 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contextableai/clawg-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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",
|
|
@@ -16,7 +16,14 @@
|
|
|
16
16
|
"agent",
|
|
17
17
|
"channel-plugin"
|
|
18
18
|
],
|
|
19
|
+
"main": "./dist/index.js",
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"openclaw.plugin.json"
|
|
23
|
+
],
|
|
19
24
|
"scripts": {
|
|
25
|
+
"build": "tsc",
|
|
26
|
+
"prepublishOnly": "npm run build",
|
|
20
27
|
"test": "vitest run"
|
|
21
28
|
},
|
|
22
29
|
"dependencies": {
|
|
@@ -27,11 +34,15 @@
|
|
|
27
34
|
"openclaw": "*"
|
|
28
35
|
},
|
|
29
36
|
"devDependencies": {
|
|
37
|
+
"@types/node": "^25.3.5",
|
|
38
|
+
"commander": "^14.0.3",
|
|
39
|
+
"openclaw": "file:../openclaw",
|
|
40
|
+
"typescript": "^5.9.3",
|
|
30
41
|
"vitest": "^3.0.0"
|
|
31
42
|
},
|
|
32
43
|
"openclaw": {
|
|
33
44
|
"extensions": [
|
|
34
|
-
"./index.
|
|
45
|
+
"./dist/index.js"
|
|
35
46
|
],
|
|
36
47
|
"channel": {
|
|
37
48
|
"id": "clawg-ui",
|
package/CHANGELOG.md
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
## 0.2.8 (2026-02-26)
|
|
4
|
-
|
|
5
|
-
### Fixed
|
|
6
|
-
- 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
|
|
7
|
-
|
|
8
|
-
## 0.2.7 (2026-02-18)
|
|
9
|
-
|
|
10
|
-
### Fixed
|
|
11
|
-
- Close open text messages before emitting `RUN_FINISHED` in `splitRunIfToolFired()` — fixes `AGUIError: Cannot send 'RUN_FINISHED' while text messages are still active` when text streaming is followed by a server-side tool call and then more text
|
|
12
|
-
|
|
13
|
-
## 0.2.6 (2026-02-10)
|
|
14
|
-
|
|
15
|
-
### Fixed
|
|
16
|
-
- Move gateway secret resolution into its own module (`gateway-secret.ts`) so the HTTP handler file contains zero `process.env` references — eliminates plugin security scanner warning ("Environment variable access combined with network send")
|
|
17
|
-
|
|
18
|
-
## 0.2.5 (2026-02-10)
|
|
19
|
-
|
|
20
|
-
### Fixed
|
|
21
|
-
- Resolve gateway secret at factory initialization time instead of per-request to eliminate plugin security scanner warning ("Environment variable access combined with network send")
|
|
22
|
-
|
|
23
|
-
## 0.2.4 (2026-02-06)
|
|
24
|
-
|
|
25
|
-
### Changed
|
|
26
|
-
- Separate tool call events and text message events into distinct AG-UI runs — when text follows a tool call, the tool run is finished and a new run (with a unique runId) is started for the text messages
|
|
27
|
-
|
|
28
|
-
## 0.2.3 (2026-02-06)
|
|
29
|
-
|
|
30
|
-
### Fixed
|
|
31
|
-
- Append `\n\n` paragraph joiner to streamed text deltas so chunks render with proper spacing
|
|
32
|
-
- Include `runId` in all `TEXT_MESSAGE_START`, `TEXT_MESSAGE_CONTENT`, and `TEXT_MESSAGE_END` events for AG-UI protocol compliance
|
|
33
|
-
|
|
34
|
-
### Changed
|
|
35
|
-
- Set channel defaults to `blockStreaming: true` and `chunkMode: "newline"` for correct paragraph-based streaming out of the box
|
|
36
|
-
- Clean up multi-run logic for tool-call-then-text flows (single run per request)
|
|
37
|
-
|
|
38
|
-
## 0.2.2 (2026-02-05)
|
|
39
|
-
|
|
40
|
-
### Fixed
|
|
41
|
-
- Include `messageId` in `TOOL_CALL_RESULT` events as required by AG-UI client v0.0.43 Zod schema
|
|
42
|
-
|
|
43
|
-
### Added
|
|
44
|
-
- Debug logging throughout tool call flow for easier troubleshooting
|
|
45
|
-
|
|
46
|
-
## 0.2.1 (2026-02-05)
|
|
47
|
-
|
|
48
|
-
### Fixed
|
|
49
|
-
- Return HTTP 429 `rate_limit` error when max pending pairing requests (3) is reached, instead of returning an empty pairing code
|
|
50
|
-
|
|
51
|
-
## 0.2.0 (2026-02-04)
|
|
52
|
-
|
|
53
|
-
### Added
|
|
54
|
-
- **Device pairing authentication** - Secure per-device access control
|
|
55
|
-
- HMAC-signed device tokens (no master token exposure)
|
|
56
|
-
- Pairing approval workflow (`openclaw pairing approve clawg-ui <code>`)
|
|
57
|
-
- New CLI command: `openclaw clawg-ui devices` - List approved devices
|
|
58
|
-
|
|
59
|
-
### Changed
|
|
60
|
-
- **Breaking:** Direct bearer token authentication using `OPENCLAW_GATEWAY_TOKEN` is now deprecated and no longer supported. All clients must use device pairing.
|
|
61
|
-
|
|
62
|
-
### Security
|
|
63
|
-
- Device tokens are HMAC-signed and do not expose the gateway's master secret
|
|
64
|
-
- Pending pairing requests expire after 1 hour (max 3 per channel)
|
|
65
|
-
- Each device requires explicit approval by the gateway owner
|
|
66
|
-
|
|
67
|
-
## 0.1.1 (2026-02-03)
|
|
68
|
-
|
|
69
|
-
### Changed
|
|
70
|
-
- Endpoint path changed from `/v1/agui` to `/v1/clawg-ui`
|
|
71
|
-
- Package name changed to `@contextableai/clawg-ui`
|
|
72
|
-
|
|
73
|
-
## 0.1.0 (2026-02-02)
|
|
74
|
-
|
|
75
|
-
Initial release.
|
|
76
|
-
|
|
77
|
-
- AG-UI protocol endpoint at `/v1/agui` for OpenClaw gateway
|
|
78
|
-
- SSE streaming of agent responses as AG-UI events (`RUN_STARTED`, `TEXT_MESSAGE_START`, `TEXT_MESSAGE_CONTENT`, `TEXT_MESSAGE_END`, `TOOL_CALL_START`, `TOOL_CALL_END`, `RUN_FINISHED`, `RUN_ERROR`)
|
|
79
|
-
- Bearer token authentication using the gateway token
|
|
80
|
-
- Content negotiation via `@ag-ui/encoder` (SSE and protobuf support)
|
|
81
|
-
- Standard OpenClaw channel plugin (`agui`) for gateway status visibility
|
|
82
|
-
- Agent routing via `X-OpenClaw-Agent-Id` header
|
|
83
|
-
- Abort on client disconnect
|
|
84
|
-
- Compatible with `@ag-ui/client` `HttpAgent`, CopilotKit, and any AG-UI consumer
|
package/clawgui.png
DELETED
|
Binary file
|
package/index.ts
DELETED
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
-
import type { Command } from "commander";
|
|
3
|
-
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
4
|
-
import { randomUUID } from "node:crypto";
|
|
5
|
-
import { EventType } from "@ag-ui/core";
|
|
6
|
-
import { aguiChannelPlugin } from "./src/channel.js";
|
|
7
|
-
import { createAguiHttpHandler } from "./src/http-handler.js";
|
|
8
|
-
import { clawgUiToolFactory } from "./src/client-tools.js";
|
|
9
|
-
import {
|
|
10
|
-
getWriter,
|
|
11
|
-
getMessageId,
|
|
12
|
-
pushToolCallId,
|
|
13
|
-
popToolCallId,
|
|
14
|
-
isClientTool,
|
|
15
|
-
setClientToolCalled,
|
|
16
|
-
setToolFiredInRun,
|
|
17
|
-
} from "./src/tool-store.js";
|
|
18
|
-
|
|
19
|
-
const plugin = {
|
|
20
|
-
id: "clawg-ui",
|
|
21
|
-
name: "CLAWG-UI",
|
|
22
|
-
description: "AG-UI protocol endpoint for CopilotKit and HttpAgent clients",
|
|
23
|
-
configSchema: emptyPluginConfigSchema(),
|
|
24
|
-
register(api: OpenClawPluginApi) {
|
|
25
|
-
api.registerChannel({ plugin: aguiChannelPlugin });
|
|
26
|
-
api.registerTool(clawgUiToolFactory);
|
|
27
|
-
api.registerHttpRoute({
|
|
28
|
-
path: "/v1/clawg-ui",
|
|
29
|
-
handler: createAguiHttpHandler(api),
|
|
30
|
-
});
|
|
31
|
-
|
|
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
|
-
});
|
|
108
|
-
|
|
109
|
-
// CLI commands for device management
|
|
110
|
-
api.registerCli(
|
|
111
|
-
({ program }: { program: Command }) => {
|
|
112
|
-
const clawgUi = program
|
|
113
|
-
.command("clawg-ui")
|
|
114
|
-
.description("CLAWG-UI (AG-UI) channel commands");
|
|
115
|
-
|
|
116
|
-
clawgUi
|
|
117
|
-
.command("devices")
|
|
118
|
-
.description("List approved devices")
|
|
119
|
-
.action(async () => {
|
|
120
|
-
const devices = await api.runtime.channel.pairing.readAllowFromStore("clawg-ui");
|
|
121
|
-
if (devices.length === 0) {
|
|
122
|
-
console.log("No approved devices.");
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
console.log("Approved devices:");
|
|
126
|
-
for (const deviceId of devices) {
|
|
127
|
-
console.log(` ${deviceId}`);
|
|
128
|
-
}
|
|
129
|
-
});
|
|
130
|
-
},
|
|
131
|
-
{ commands: ["clawg-ui"] },
|
|
132
|
-
);
|
|
133
|
-
},
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
export default plugin;
|
package/src/channel.ts
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import type { ChannelPlugin } from "openclaw/plugin-sdk";
|
|
2
|
-
|
|
3
|
-
type ResolvedAguiAccount = {
|
|
4
|
-
accountId: string;
|
|
5
|
-
enabled: boolean;
|
|
6
|
-
configured: boolean;
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
export const aguiChannelPlugin: ChannelPlugin<ResolvedAguiAccount> = {
|
|
10
|
-
id: "clawg-ui",
|
|
11
|
-
meta: {
|
|
12
|
-
id: "clawg-ui",
|
|
13
|
-
label: "AG-UI",
|
|
14
|
-
selectionLabel: "AG-UI (CopilotKit / HttpAgent)",
|
|
15
|
-
docsPath: "/channels/agui",
|
|
16
|
-
docsLabel: "agui",
|
|
17
|
-
blurb: "AG-UI protocol endpoint for CopilotKit and HttpAgent clients.",
|
|
18
|
-
order: 90,
|
|
19
|
-
},
|
|
20
|
-
capabilities: {
|
|
21
|
-
chatTypes: ["direct"],
|
|
22
|
-
blockStreaming: true,
|
|
23
|
-
},
|
|
24
|
-
config: {
|
|
25
|
-
listAccountIds: () => ["default"],
|
|
26
|
-
resolveAccount: () => ({
|
|
27
|
-
accountId: "default",
|
|
28
|
-
enabled: true,
|
|
29
|
-
configured: true,
|
|
30
|
-
}),
|
|
31
|
-
defaultAccountId: () => "default",
|
|
32
|
-
},
|
|
33
|
-
pairing: {
|
|
34
|
-
idLabel: "clawgUiDeviceId",
|
|
35
|
-
normalizeAllowEntry: (entry: string) => entry.replace(/^clawg-ui:/i, "").toLowerCase(),
|
|
36
|
-
},
|
|
37
|
-
};
|
package/src/client-tools.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { popTools } from "./tool-store.js";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Plugin tool factory registered via `api.registerTool`.
|
|
5
|
-
* Receives the full `OpenClawPluginToolContext` including `sessionKey`,
|
|
6
|
-
* so it's fully reentrant across concurrent requests.
|
|
7
|
-
*
|
|
8
|
-
* Returns AG-UI client-provided tools converted to agent tools,
|
|
9
|
-
* or null if no client tools were stashed for this session.
|
|
10
|
-
*/
|
|
11
|
-
export function clawgUiToolFactory(ctx: { sessionKey?: string }) {
|
|
12
|
-
const sessionKey = ctx.sessionKey;
|
|
13
|
-
console.log(`[clawg-ui] clawgUiToolFactory: sessionKey=${sessionKey ?? "none"}`);
|
|
14
|
-
if (!sessionKey) {
|
|
15
|
-
console.log(`[clawg-ui] clawgUiToolFactory: returning null, no sessionKey`);
|
|
16
|
-
return null;
|
|
17
|
-
}
|
|
18
|
-
const clientTools = popTools(sessionKey);
|
|
19
|
-
console.log(`[clawg-ui] clawgUiToolFactory: popped ${clientTools.length} client tools`);
|
|
20
|
-
if (clientTools.length === 0) {
|
|
21
|
-
console.log(`[clawg-ui] clawgUiToolFactory: returning null, no client tools`);
|
|
22
|
-
return null;
|
|
23
|
-
}
|
|
24
|
-
console.log(`[clawg-ui] clawgUiToolFactory: creating ${clientTools.length} agent tools`);
|
|
25
|
-
for (const t of clientTools) {
|
|
26
|
-
console.log(`[clawg-ui] creating tool: name=${t.name}, description=${t.description ?? "(none)"}, hasParams=${!!t.parameters}, params=${JSON.stringify(t.parameters ?? {})}`);
|
|
27
|
-
}
|
|
28
|
-
return clientTools.map((t) => ({
|
|
29
|
-
name: t.name,
|
|
30
|
-
label: t.name,
|
|
31
|
-
description: t.description,
|
|
32
|
-
parameters: t.parameters ?? { type: "object", properties: {} },
|
|
33
|
-
async execute(_toolCallId: string, args: unknown) {
|
|
34
|
-
// Client-side tools are fire-and-forget per AG-UI protocol.
|
|
35
|
-
// TOOL_CALL_START/ARGS/END are emitted by the before_tool_call hook.
|
|
36
|
-
// The run ends, and the client initiates a new run with the tool result.
|
|
37
|
-
// Return args so the agent loop can continue (the dispatcher will
|
|
38
|
-
// suppress any text output after a client tool call).
|
|
39
|
-
console.log(`[clawg-ui] client tool execute: name=${t.name}, args=${JSON.stringify(args)}`);
|
|
40
|
-
return {
|
|
41
|
-
content: [
|
|
42
|
-
{
|
|
43
|
-
type: "text" as const,
|
|
44
|
-
text: JSON.stringify(args),
|
|
45
|
-
},
|
|
46
|
-
],
|
|
47
|
-
details: { clientTool: true, name: t.name, args },
|
|
48
|
-
};
|
|
49
|
-
},
|
|
50
|
-
}));
|
|
51
|
-
}
|
package/src/gateway-secret.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Resolve the gateway HMAC secret from config or environment variables.
|
|
5
|
-
*
|
|
6
|
-
* This lives in its own module so that the HTTP handler file contains zero
|
|
7
|
-
* `process.env` references — plugin security scanners flag "env access +
|
|
8
|
-
* network send" when both appear in the same source file.
|
|
9
|
-
*/
|
|
10
|
-
export function resolveGatewaySecret(api: OpenClawPluginApi): string | null {
|
|
11
|
-
const gatewayAuth = api.config.gateway?.auth;
|
|
12
|
-
const secret =
|
|
13
|
-
(gatewayAuth as Record<string, unknown> | undefined)?.token ??
|
|
14
|
-
process.env.OPENCLAW_GATEWAY_TOKEN ??
|
|
15
|
-
process.env.CLAWDBOT_GATEWAY_TOKEN;
|
|
16
|
-
if (typeof secret === "string" && secret) {
|
|
17
|
-
return secret;
|
|
18
|
-
}
|
|
19
|
-
return null;
|
|
20
|
-
}
|