AG-UI protocol channel plugin for OpenClaw — connect CopilotKit and AG-UI clients to your OpenClaw gateway
npm install @contextableai/clawg-uiAn 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.
``bash`
npm install @contextableai/clawg-ui
Or with the OpenClaw plugin CLI:
`bash`
openclaw plugins install @contextableai/clawg-ui
Then 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:
1. Authenticates the request using device pairing (see Authentication)
2. Parses the AG-UI messages into an OpenClaw inbound context
3. Routes to the appropriate agent via the gateway's standard routing
4. Dispatches the message through the reply pipeline (same path as Telegram, Teams, etc.)
5. 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)
`bash`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?"}
]
}'
`typescript
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);
}
`
`tsx
import { CopilotKit } from "@copilotkit/react-core";
// Device token obtained through the pairing flow
const deviceToken = process.env.CLAWG_UI_DEVICE_TOKEN;
function App() {
return (
headers={{
Authorization: Bearer ${deviceToken},`
}}
>
{/ your app /}
);
}
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). |
`json`
{
"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
│ │ │
│ │ 6. Success - SSE stream│
│ │────────────────────────>│
│ │ │
#### 1. Client initiates pairing
The client sends a POST request without any authorization header:
`bash`
curl -X POST http://localhost:18789/v1/clawg-ui \
-H "Content-Type: application/json" \
-d '{}'
Response (403):
`json`
{
"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.
#### 2. Approve the device (gateway owner)
The client shares the pairingCode with the gateway owner, who approves it:
`bashList pending pairing requests
openclaw pairing list clawg-ui
#### 3. Client uses Bearer token
Once approved, the client uses their Bearer token for all requests:
`bash
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"}]}'
`$3
| 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 | Approve a device by its pairing code |$3
| Status | Type | Meaning |
|--------|------|---------|
| 401 |
unauthorized | Invalid device token |
| 403 | pairing_pending | No auth (initiates pairing) or valid token but device not yet approved |$3
> 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_TOKEN
> `
>
> New (required):
> `
> POST without Authorization header # Initiates pairing
> Authorization: Bearer # After approval
> `Agent routing
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:`bash
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"}]}'
`Error responses
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.Development
`bash
git clone https://github.com/contextablemark/clawg-ui
cd clawg-ui
npm install
npm test
``License
MIT