MCPcopy Index your code
hub / github.com/contextablemark/clawg-ui

github.com/contextablemark/clawg-ui @v0.7.0

Chat with this repo
repository ↗ · DeepWiki ↗ · release v0.7.0 ↗ · + Follow
100 symbols 239 edges 25 files 1 documented · 1%
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

clawg-ui

Banner

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.

Companion: @contextableai/clawpilotkit

If you want a ready-made chat UI that talks to this plugin, the companion package @contextableai/clawpilotkit ships the same prebuilt React + CopilotKit bundle in two modes:

  • Embedded in the OpenClaw operator console as a chat.surface slot (renders inside the Chat tab in place of the built-in message thread + input box, no pairing needed).
  • Standalone via npx @contextableai/clawpilotkit against any clawg-ui ≥ 0.7.0 gateway (the launcher walks the user through device pairing on first connect).

See clawpilotkit/README.md for setup. The rest of this document covers the gateway-side plugin, which is what clawpilotkit (and any other AG-UI client) talks to.

Installation

npm install @contextableai/clawg-ui

Or with the OpenClaw plugin CLI:

openclaw plugins install @contextableai/clawg-ui

Then restart the gateway. The plugin auto-registers the /v1/clawg-ui endpoint and the clawg-ui channel.

How it works

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_ARGS                |
    |<-------------------------------------|
    |  SSE: TOOL_CALL_RESULT              |
    |<-------------------------------------|  (server tools only)
    |  SSE: TOOL_CALL_END                 |
    |<-------------------------------------|
    |  SSE: TEXT_MESSAGE_END              |
    |<-------------------------------------|
    |  SSE: RUN_FINISHED                  |
    |<-------------------------------------|

Usage

Prerequisites

  • OpenClaw gateway running (openclaw gateway run)
  • A paired device token (see Authentication)

curl

# 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?"}
    ]
  }'

@ag-ui/client HttpAgent

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);
}

CopilotKit

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>
  );
}

Request format

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. May be empty (returns an empty run). For agent execution, at least one user or tool message should be present.
tools Tool[] no Client-side tool definitions. The agent can invoke these; see Tool call events.
state object no Client state (reserved for future use).

Message format

{
  "role": "user",
  "content": "Hello"
}

Supported roles: user, assistant, system, tool.

Response format

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_ARGS Tool call arguments (JSON delta)
TOOL_CALL_RESULT Server-side tool execution result
TOOL_CALL_END Tool call complete
RUN_FINISHED Agent run complete
RUN_ERROR On failure

Tool call events

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.

When do tool events appear?

  • The agent must have tools available (server-side tools on the agent, or client-side tools passed via the tools field)
  • The agent's LLM must decide to call a tool based on the conversation

Client tools vs server tools:

  • Client tools (passed via tools in the request): the stream emits TOOL_CALL_STARTTOOL_CALL_ARGSTOOL_CALL_END, then the run finishes. The client executes the tool locally and starts a new run with the result as a tool message.
  • Server tools (registered on the OpenClaw agent): the stream emits TOOL_CALL_STARTTOOL_CALL_ARGSTOOL_CALL_RESULTTOOL_CALL_END. The agent continues processing in the same or a subsequent run.

Tip: To confirm tool calls are being triggered, check the gateway logs for [clawg-ui] before_tool_call: entries.

Authentication

clawg-ui uses device pairing to authenticate clients. This provides secure, per-device access control without exposing the gateway's master token.

Device Pairing Flow

┌─────────────────┐      ┌──────────────────┐      ┌─────────────────┐
│  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│
         │                        │────────────────────────>│
         │                        │                         │

Step-by-Step Setup

1. Client initiates pairing

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.

2. Approve the device (gateway owner)

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 ABCD1234

3. Client uses Bearer token

Once 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"}]}'

CLI Commands

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

Error Responses

Status Type Meaning
401 unauthorized Invalid device token
403 pairing_pending No auth (initiates pairing) or valid token but device not yet approved

Deprecated: Direct Bearer Token

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 <device-token> # 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:

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"}]}'

Session isolation

By default, sessions are keyed by route.sessionKey plus a :thread:<threadId> suffix so each thread gets its own session within the device. For multi-user applications where each user needs isolated conversation history within a shared AG-UI client, pass the X-OpenClaw-Session-Key header to add a :user:<value> scope on top:

curl -N -X POST http://localhost:18789/v1/clawg-ui \
  -H "Authorization: Bearer $CLAWG_UI_DEVICE_TOKEN" \
  -H "X-OpenClaw-Session-Key: user@example.com" \
  -d '{"messages":[{"role":"user","content":"Hello"}]}'

This is useful when: - Multiple authenticated users share the same AG-UI client (e.g., a web app with auth) - You want to key sessions by user identity in addition to thread ID - CopilotKit or other AG-UI clients manage threadId internally

How the final session key is composed

The header scopes the route-derived key; it does not replace it:

<route.sessionKey>[:user:<header>][:thread:<threadId>]

With no header, the session key is <route.sessionKey>:thread:<threadId> (existing behaviour). With the header set to alice@example.com and threadId: "t-1", it becomes <route.sessionKey>:user:alice@example.com:thread:t-1. The header can only subdivide an existing route scope — it cannot escape it.

Trust model — treat this header like X-Forwarded-For

X-OpenClaw-Session-Key is a trusted-proxy-only concern, in the same family as X-Forwarded-For and X-Request-ID. It is expected to be set by a reverse proxy or authentication middleware that has already authenticated the user — not by end clients.

Deployments that are reachable by untrusted clients must strip or overwrite this header at the ingress edge before forwarding to the gateway. If an end client can set this header freely, they can impersonate any other user's session scope for that same device.

Validation

The header value must match these rules; inv

Extension points exported contracts — how you extend this code

BeforeToolCallEvent (Interface)
(no doc)
index.ts
SSEEvent (Interface)
(no doc)
src/integration.test.ts
AuthenticatedCaller (Interface)
(no doc)
src/http-handler.ts
CronRun (Interface)
(no doc)
examples/cron-report-tool.ts
ToolCallContext (Interface)
(no doc)
index.ts

Core symbols most depended-on inside this repo

writeEvent
called by 19
src/http-handler.ts
handleBeforeToolCall
called by 11
index.ts
createAguiHttpHandler
called by 9
src/http-handler.ts
extractToolResultText
called by 8
src/a2ui.ts
tryParseA2UIOperations
called by 8
src/a2ui.ts
sendJson
called by 8
src/http-handler.ts
handleToolResultPersist
called by 6
index.ts
getOperationSurfaceId
called by 6
src/a2ui.ts

Shape

Function 95
Interface 5

Languages

TypeScript100%

Modules by API surface

src/tool-store.ts17 symbols
src/http-handler.ts17 symbols
src/http-handler.test.ts9 symbols
clawpilotkit/app/src/Standalone.tsx9 symbols
clawpilotkit/app/src/openclaw-bridge.ts8 symbols
src/integration.test.ts5 symbols
index.ts5 symbols
clawpilotkit/bin/clawpilotkit.js5 symbols
src/a2ui.ts4 symbols
clawpilotkit/src/static-ui-handler.ts4 symbols
examples/cron-report-tool.ts3 symbols
clawpilotkit/app/src/pairing.ts3 symbols

For agents

$ claude mcp add clawg-ui \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact