An OpenClaw channel plugin that exposes the gateway as an AG-UI protocol-compatible HTTP endpoint. AG-UI clients such as CopilotKit UIs and @ag-ui/client HttpAgent instances can connect to OpenClaw and receive streamed responses.
npm install @contextableai/clawg-uiOr with the OpenClaw plugin CLI:
openclaw plugins install @contextableai/clawg-uiThen restart the gateway. The plugin auto-registers the /v1/clawg-ui endpoint and the clawg-ui channel.
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:
- Authenticates the request using device pairing (see Authentication)
- Parses the AG-UI messages into an OpenClaw inbound context
- Routes to the appropriate agent via the gateway's standard routing
- Dispatches the message through the reply pipeline (same path as Telegram, Teams, etc.)
- Streams the agent's response back as AG-UI SSE events
AG-UI Client OpenClaw Gateway
| |
| POST /v1/clawg-ui (RunAgentInput) |
|------------------------------------->|
| | Auth (device token)
| | Route to agent
| | Dispatch inbound message
| |
| SSE: RUN_STARTED |
|<-------------------------------------|
| SSE: TEXT_MESSAGE_START |
|<-------------------------------------|
| SSE: TEXT_MESSAGE_CONTENT (delta) |
|<-------------------------------------| (streamed chunks)
| SSE: TEXT_MESSAGE_CONTENT (delta) |
|<-------------------------------------|
| SSE: TOOL_CALL_START |
|<-------------------------------------| (if agent uses tools)
| SSE: TOOL_CALL_END |
|<-------------------------------------|
| SSE: TEXT_MESSAGE_END |
|<-------------------------------------|
| SSE: RUN_FINISHED |
|<-------------------------------------|
- OpenClaw gateway running (
openclaw gateway run) - A paired device token (see Authentication)
# Using your device token (obtained through pairing)
curl -N -X POST http://localhost:18789/v1/clawg-ui \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-H "Authorization: Bearer $CLAWG_UI_DEVICE_TOKEN" \
-d '{
"threadId": "thread-1",
"runId": "run-1",
"messages": [
{"role": "user", "content": "What is the weather in San Francisco?"}
]
}'import { HttpAgent } from "@ag-ui/client";
// Device token obtained through the pairing flow
const deviceToken = process.env.CLAWG_UI_DEVICE_TOKEN;
const agent = new HttpAgent({
url: "http://localhost:18789/v1/clawg-ui",
headers: {
Authorization: `Bearer ${deviceToken}`,
},
});
const stream = agent.run({
threadId: "thread-1",
runId: "run-1",
messages: [
{ role: "user", content: "Hello from CLAWG-UI" },
],
});
for await (const event of stream) {
console.log(event.type, event);
}import { CopilotKit } from "@copilotkit/react-core";
// Device token obtained through the pairing flow
const deviceToken = process.env.CLAWG_UI_DEVICE_TOKEN;
function App() {
return (
<CopilotKit
runtimeUrl="http://localhost:18789/v1/clawg-ui"
headers={{
Authorization: `Bearer ${deviceToken}`,
}}
>
{/* your app */}
</CopilotKit>
);
}The endpoint accepts a POST with a JSON body matching the AG-UI RunAgentInput schema:
| Field | Type | Required | Description |
|---|---|---|---|
threadId |
string | no | Conversation thread ID. Auto-generated if omitted. |
runId |
string | no | Unique run ID. Auto-generated if omitted. |
messages |
Message[] | yes | Array of messages. At least one user message required. |
tools |
Tool[] | no | Client-side tool definitions (reserved for future use). |
state |
object | no | Client state (reserved for future use). |
{
"role": "user",
"content": "Hello"
}Supported roles: user, assistant, system, tool.
The response is an SSE stream. Each event is a data: line containing a JSON object with a type field from the AG-UI EventType enum:
| Event | When |
|---|---|
RUN_STARTED |
Immediately after validation |
TEXT_MESSAGE_START |
First assistant text chunk |
TEXT_MESSAGE_CONTENT |
Each streamed text delta |
TEXT_MESSAGE_END |
After last text chunk |
TOOL_CALL_START |
Agent invokes a tool |
TOOL_CALL_END |
Tool execution complete |
RUN_FINISHED |
Agent run complete |
RUN_ERROR |
On failure |
clawg-ui uses device pairing to authenticate clients. This provides secure, per-device access control without exposing the gateway's master token.
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Gateway Owner │ │ OpenClaw Server │ │ AG-UI Client │
└────────┬────────┘ └────────┬─────────┘ └────────┬────────┘
│ │ │
│ │ 1. POST (no auth) │
│ │<────────────────────────│
│ │ │
│ │ 2. Return device token │
│ │ + pairing code │
│ │────────────────────────>│
│ │ 403 pairing_pending │
│ │ { pairingCode, token }
│ │ │
│ 3. Share pairing code (out of band) │
│<─────────────────────────────────────────────────│
│ │ │
4. Approve device │ │
│ openclaw pairing approve clawg-ui ABCD1234 │
│───────────────────────>│ │
│ │ │
│ │ 5. POST with device token
│ │<────────────────────────│
│ │ Authorization: Bearer <token>
│ │ │
│ │ 6. Success - SSE stream│
│ │────────────────────────>│
│ │ │
The client sends a POST request without any authorization header:
curl -X POST http://localhost:18789/v1/clawg-ui \
-H "Content-Type: application/json" \
-d '{}'Response (403):
{
"error": {
"type": "pairing_pending",
"message": "Device pending approval",
"pairing": {
"pairingCode": "ABCD1234",
"token": "MmRlOTA0ODIt...b71d",
"instructions": "Save this token for use as a Bearer token and ask the owner to approve: openclaw pairing approve clawg-ui ABCD1234"
}
}
}The client must save the token for future requests.
The client shares the pairingCode with the gateway owner, who approves it:
# List pending pairing requests
openclaw pairing list clawg-ui
# Approve the device
openclaw pairing approve clawg-ui ABCD1234Once approved, the client uses their Bearer token for all requests:
curl -N -X POST http://localhost:18789/v1/clawg-ui \
-H "Authorization: Bearer MmRlOTA0ODIt...b71d" \
-H "Content-Type: application/json" \
-d '{"messages":[{"role":"user","content":"Hello"}]}'| Command | Description |
|---|---|
openclaw clawg-ui devices |
List approved devices |
openclaw pairing list clawg-ui |
List pending pairing requests awaiting approval |
openclaw pairing approve clawg-ui <code> |
Approve a device by its pairing code |
| Status | Type | Meaning |
|---|---|---|
| 401 | unauthorized |
Invalid device token |
| 403 | pairing_pending |
No auth (initiates pairing) or valid token but device not yet approved |
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.Old (deprecated):
Authorization: Bearer $OPENCLAW_GATEWAY_TOKENNew (required):
POST without Authorization header # Initiates pairing Authorization: Bearer <device-token> # After approval
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:
curl -N -X POST http://localhost:18789/v1/clawg-ui \
-H "Authorization: Bearer $CLAWG_UI_DEVICE_TOKEN" \
-H "X-OpenClaw-Agent-Id: my-agent" \
-d '{"messages":[{"role":"user","content":"Hello"}]}'Non-streaming errors return JSON:
| Status | Type | Meaning |
|---|---|---|
| 400 | invalid_request_error |
Invalid request (missing messages, bad JSON) |
| 401 | unauthorized |
Invalid device token |
| 403 | pairing_pending |
No auth header (initiates pairing) or valid token but device not yet approved |
| 405 | — | Method not allowed (only POST accepted) |
Streaming errors emit a RUN_ERROR event and close the connection.
git clone https://github.com/contextablemark/clawg-ui
cd clawg-ui
npm install
npm testMIT
