Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions websocket/openclaw/.imposter.yaml
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
# The OpenClaw mock is split across several '*-config.yaml' files (one per
# protocol domain); Imposter loads and merges them all. See README.md.
engine: native
143 changes: 116 additions & 27 deletions websocket/openclaw/README.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,44 @@
# WebSocket example: OpenClaw Gateway simulation

Simulates a subset of the [OpenClaw Gateway WebSocket protocol](https://docs.openclaw.ai/gateway/protocol) — enough for an operator client (e.g. the lucinate TUI) to connect, browse a single agent and session, and hold a simple chat:
Simulates the operator-facing surface of the [OpenClaw Gateway WebSocket protocol](https://docs.openclaw.ai/gateway/protocol) — enough for an operator client (e.g. the lucinate TUI) to connect, browse agents/sessions/tools/tasks, hold a streamed chat, and drive most read and write RPCs against a coherent, self-consistent world.

1. On connection, the server immediately sends a `connect.challenge` event and starts a periodic `tick` keepalive every 15 seconds. The `ts`/`timestamp` fields are numbers (epoch millis), matching the protocol's `int64` typing.
2. A `connect` request frame is answered with a `hello-ok` response (protocol 4), echoing the request's `id`. The hello snapshot advertises a `main` agent and its `main` session.
3. An `agents.list` request returns a single agent, `main` (also the default).
4. A `sessions.list` request returns one `main` session.
5. A `cron.list` request returns no jobs.
6. A `sessions.create` request returns the `main` session key (there is a single session, so create/resume always lands on `main`).
7. A `chat.history` request returns a short seed conversation, so the chat screen has something to render. Return `{"messages":[]}` for an empty history.
8. A `models.list` request returns a few models. The `main` session's current model (from `sessions.list`) is one of them, so the model picker highlights it.
9. A `sessions.patch` request (used to set the session model, thinking level, etc.) is acknowledged with an `ok` `res` frame — it is a void RPC, so no payload is needed.
10. A `chat.send` request is acknowledged with a `res` frame (`runId`/`status`), followed by streamed `agent` (tool start/result) and `chat` (`delta` then `final`) events with realistic delays. The `runId` is the request id and the `sessionKey` is echoed from the request, so the client routes the events to the active run.
Frame shapes follow the protocol's canonical schemas (`req`/`res`/`event` envelopes; methods echo the request `id`; `ts`/timestamp fields are numbers — epoch millis — matching the `int64` typing).

The resources use a wildcard path (`path: /*`), so the mock accepts a WebSocket connection on any path — mirroring the real OpenClaw gateway, which multiplexes on a single port and routes the upgrade by the `Upgrade: websocket` header rather than a specific URL path. Connect on whatever path your client uses (e.g. `/ws`).
## How it's organised

The mock is **split across several `*-config.yaml` files, one per protocol domain**:

| File | Domain |
| --- | --- |
| `imposter-config.yaml` | Connection core: `on: open` challenge + tick, `connect` handshake, unknown-method fallback |
| `sessions-config.yaml` | `sessions.*` |
| `chat-config.yaml` | `chat.*` (incl. the streamed `chat.send`) |
| `agents-config.yaml` | `agents.*`, `agent.*` |
| `models-config.yaml` | `models.*` |
| `cron-config.yaml` | `cron.*` |
| `tools-config.yaml` | `commands.*`, `tools.*`, `skills.*` |
| `channels-config.yaml` | `channels.*` |
| `tasks-config.yaml` | `tasks.*`, `artifacts.*` |
| `system-config.yaml` | `system-info`, `audit.*`, `config.get`, pairing/node/environment/approval lists |

Imposter loads every `*-config.yaml` in the directory. Because they all declare `plugin: websocket` on the same wildcard `path: /*`, the engine **coalesces them into a single handler** — so one multiplexed connection serves every method, whichever file defines it. (This mirrors the real gateway, which multiplexes all methods over one connection and routes the upgrade by the `Upgrade: websocket` header rather than a URL path. Connect on whatever path your client uses, e.g. `/ws`.)

## The mock world

Everything is consistent with a single-agent, single-session gateway:

- **Agent** `main` (the default) — see `agents.list`, `agent.identity.get`.
- **Session** `main`, titled "Main conversation", currently on `claude-opus-4-8`.
- **Models** `claude-opus-4-8`, `claude-sonnet-5`, `claude-haiku-4-5` (the session's model is one of them, so the picker highlights it).
- **One cron job** `cron-daily-digest`, consistent across `cron.list` / `cron.get` / `cron.runs`.
- **One completed task** `task-1` and **one code artifact** `artifact-1`, both in session `main`.
- **One connected Telegram account** for `channels.status`.
- Pairing, node, environment and approval lists are empty (as on a simple local gateway).

Read methods return this snapshot; write/void methods are acknowledged with an `ok:true` response frame. Any method the mock does not implement returns a canonical `method_not_found` error frame.

> [!NOTE]
> A few gateway results are not pinned by the wire schema (e.g. `sessions.list`, `sessions.usage`, `cron.status`). Their payloads here are representative shapes consistent with the reference client, sufficient to render a client UI.

## Run

Expand All @@ -38,45 +63,109 @@ Using [websocat](https://github.com/vi/websocat):
websocat ws://localhost:8080/ws
```

You'll receive the challenge event immediately. Then paste a handshake:
You'll receive the `connect.challenge` event immediately, and a `tick` keepalive every 15 seconds. Then paste the handshake:

```json
{"type":"req","id":"req-1","method":"connect","params":{"minProtocol":3,"maxProtocol":4,"client":{"id":"cli","version":"0.1.0","platform":"go","mode":"cli"}}}
```

to receive `hello-ok`. List the agent, session and cron jobs:
to receive `hello-ok`. Its `features.methods` advertises everything the mock implements.

### Agents

```json
{"type":"req","id":"a-1","method":"agents.list","params":{}}
{"type":"req","id":"a-2","method":"agent.identity.get","params":{"agentId":"main"}}
{"type":"req","id":"a-3","method":"agents.files.list","params":{"agentId":"main"}}
{"type":"req","id":"a-4","method":"agents.workspace.list","params":{"agentId":"main"}}
```

### Sessions

```json
{"type":"req","id":"s-1","method":"sessions.list","params":{"agentId":"main"}}
{"type":"req","id":"s-2","method":"sessions.get","params":{"key":"main"}}
{"type":"req","id":"s-3","method":"sessions.preview","params":{"keys":["main"]}}
{"type":"req","id":"s-4","method":"sessions.create","params":{"agentId":"main","key":"main"}}
{"type":"req","id":"s-5","method":"sessions.patch","params":{"key":"main","model":"claude-sonnet-5"}}
{"type":"req","id":"s-6","method":"sessions.groups.list","params":{}}
```

### Chat

```json
{"type":"req","id":"h-1","method":"chat.history","params":{"sessionKey":"main","limit":50}}
{"type":"req","id":"h-2","method":"chat.message.get","params":{"sessionKey":"main","messageId":"msg-2"}}
{"type":"req","id":"h-3","method":"chat.send","params":{"sessionKey":"main","message":"hello","idempotencyKey":"idem-1"}}
```

`chat.send` returns an acknowledgement (`runId`/`status`), then streams `agent` tool events (`start` then `result`) and `chat` events (a `delta` carrying `deltaText`, then a `final` message with `stopReason`). The `runId` is the request id and the `sessionKey` is echoed, so the client routes the events to the active run.

### Models

```json
{"type":"req","id":"m-1","method":"models.list","params":{}}
{"type":"req","id":"m-2","method":"models.probe","params":{"provider":"anthropic"}}
```

### Cron

```json
{"type":"req","id":"req-2","method":"agents.list","params":{}}
{"type":"req","id":"req-3","method":"sessions.list","params":{"agentId":"main"}}
{"type":"req","id":"req-4","method":"cron.list","params":{"enabled":"all"}}
{"type":"req","id":"cr-1","method":"cron.list","params":{"enabled":"all"}}
{"type":"req","id":"cr-2","method":"cron.get","params":{"id":"cron-daily-digest"}}
{"type":"req","id":"cr-3","method":"cron.runs","params":{"id":"cron-daily-digest"}}
```

open the session and load its history:
### Commands, tools and skills

```json
{"type":"req","id":"req-5","method":"sessions.create","params":{"agentId":"main","key":"main"}}
{"type":"req","id":"req-6","method":"chat.history","params":{"sessionKey":"main","limit":50}}
{"type":"req","id":"t-1","method":"commands.list","params":{"agentId":"main"}}
{"type":"req","id":"t-2","method":"tools.catalog","params":{"agentId":"main"}}
{"type":"req","id":"t-3","method":"tools.effective","params":{"sessionKey":"main"}}
{"type":"req","id":"t-4","method":"skills.status","params":{"agentId":"main"}}
{"type":"req","id":"t-5","method":"skills.search","params":{"query":"web"}}
```

list models and switch the session's model:
### Channels

```json
{"type":"req","id":"req-7","method":"models.list","params":{}}
{"type":"req","id":"req-8","method":"sessions.patch","params":{"key":"main","model":"claude-sonnet-5"}}
{"type":"req","id":"ch-1","method":"channels.status","params":{}}
```

and send a chat message:
### Tasks and artifacts

```json
{"type":"req","id":"tk-1","method":"tasks.list","params":{}}
{"type":"req","id":"tk-2","method":"tasks.get","params":{"taskId":"task-1"}}
{"type":"req","id":"ar-1","method":"artifacts.list","params":{"sessionKey":"main"}}
{"type":"req","id":"ar-2","method":"artifacts.download","params":{"artifactId":"artifact-1"}}
```

### System, audit and config

```json
{"type":"req","id":"sy-1","method":"system-info","params":{}}
{"type":"req","id":"sy-2","method":"audit.activity.list","params":{}}
{"type":"req","id":"sy-3","method":"config.get","params":{}}
```

### Unknown methods

Any method not implemented by the mock returns an error frame:

```json
{"type":"req","id":"x-1","method":"does.not.exist","params":{}}
```

```json
{"type":"req","id":"req-9","method":"chat.send","params":{"sessionKey":"main","message":"hello","idempotencyKey":"idem-1"}}
{"type":"res","id":"x-1","ok":false,"error":{"code":"method_not_found","message":"This method is not implemented by the mock."}}
```

to receive an acknowledgement followed by streamed tool and chat events. Leave the connection open to observe the periodic `tick` events.
Leave the connection open to keep observing the periodic `tick` events.

### Using a real OpenClaw client

We can test this interactively using a real OpenCalw client, like [lucinate](https://github.com/lucinate-ai/lucinate).
We can test this interactively using a real OpenClaw client, like [lucinate](https://github.com/lucinate-ai/lucinate).

```sh
brew install lucinate-ai/tap/lucinate
Expand Down
12 changes: 12 additions & 0 deletions websocket/openclaw/agent-identity.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"type": "res",
"id": "${stores.request.reqId}",
"ok": true,
"payload": {
"agentId": "main",
"name": "Main",
"emoji": "🤖",
"avatarSource": "emoji",
"avatarStatus": "emoji"
}
}
154 changes: 154 additions & 0 deletions websocket/openclaw/agents-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# OpenClaw Gateway WebSocket mock - agents domain.
# A single 'main' agent, its identity, bootstrap files and workspace.
plugin: websocket

resources:
# agents.list: a single agent, 'main', which is also the default.
- path: /*
requestBody:
allOf:
- jsonPath: $.type
value: req
- jsonPath: $.method
value: agents.list
capture:
reqId: { requestBody: { jsonPath: $.id } }
response:
content: '{"type":"res","id":"${stores.request.reqId}","ok":true,"payload":{"defaultId":"main","mainKey":"main","scope":"global","agents":[{"id":"main","name":"main"}]}}'
template: true

# agent.identity.get: the effective identity for the main agent.
- path: /*
requestBody:
allOf:
- jsonPath: $.type
value: req
- jsonPath: $.method
value: agent.identity.get
capture:
reqId: { requestBody: { jsonPath: $.id } }
response:
file: agent-identity.json
template: true

# agents.files.list / get: bootstrap files for the main agent.
- path: /*
requestBody:
allOf:
- jsonPath: $.type
value: req
- jsonPath: $.method
value: agents.files.list
capture:
reqId: { requestBody: { jsonPath: $.id } }
response:
file: agents-files-list.json
template: true

- path: /*
requestBody:
allOf:
- jsonPath: $.type
value: req
- jsonPath: $.method
value: agents.files.get
capture:
reqId: { requestBody: { jsonPath: $.id } }
response:
file: agents-file-get.json
template: true

# agents.workspace.list / get: browse the main agent's workspace.
- path: /*
requestBody:
allOf:
- jsonPath: $.type
value: req
- jsonPath: $.method
value: agents.workspace.list
capture:
reqId: { requestBody: { jsonPath: $.id } }
response:
file: agents-workspace-list.json
template: true

- path: /*
requestBody:
allOf:
- jsonPath: $.type
value: req
- jsonPath: $.method
value: agents.workspace.get
capture:
reqId: { requestBody: { jsonPath: $.id } }
response:
file: agents-workspace-get.json
template: true

# agent.wait: terminal snapshot for a run.
- path: /*
requestBody:
allOf:
- jsonPath: $.type
value: req
- jsonPath: $.method
value: agent.wait
capture:
reqId: { requestBody: { jsonPath: $.id } }
response:
content: '{"type":"res","id":"${stores.request.reqId}","ok":true,"payload":{"runId":"run-1","status":"completed","stopReason":"end_turn"}}'
template: true

# agents.create / update / delete: mutations, acknowledged with a result.
- path: /*
requestBody:
allOf:
- jsonPath: $.type
value: req
- jsonPath: $.method
value: agents.create
capture:
reqId: { requestBody: { jsonPath: $.id } }
response:
content: '{"type":"res","id":"${stores.request.reqId}","ok":true,"payload":{"ok":true,"agentId":"main","name":"Main","workspace":"/home/mock/.openclaw/agents/main","model":"claude-opus-4-8"}}'
template: true

- path: /*
requestBody:
allOf:
- jsonPath: $.type
value: req
- jsonPath: $.method
value: agents.update
capture:
reqId: { requestBody: { jsonPath: $.id } }
response:
content: '{"type":"res","id":"${stores.request.reqId}","ok":true,"payload":{"ok":true,"agentId":"main"}}'
template: true

- path: /*
requestBody:
allOf:
- jsonPath: $.type
value: req
- jsonPath: $.method
value: agents.delete
capture:
reqId: { requestBody: { jsonPath: $.id } }
response:
content: '{"type":"res","id":"${stores.request.reqId}","ok":true,"payload":{"ok":true,"agentId":"main","removedBindings":0}}'
template: true

# agents.files.set: write a bootstrap file, acknowledged with the file result.
- path: /*
requestBody:
allOf:
- jsonPath: $.type
value: req
- jsonPath: $.method
value: agents.files.set
capture:
reqId: { requestBody: { jsonPath: $.id } }
response:
content: '{"type":"res","id":"${stores.request.reqId}","ok":true,"payload":{"ok":true,"agentId":"main","workspace":"/home/mock/.openclaw/agents/main","file":{"name":"AGENT.md","path":"/home/mock/.openclaw/agents/main/AGENT.md","missing":false,"size":128,"updatedAtMs":${datetime.now.millis}}}}'
template: true
17 changes: 17 additions & 0 deletions websocket/openclaw/agents-file-get.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"type": "res",
"id": "${stores.request.reqId}",
"ok": true,
"payload": {
"agentId": "main",
"workspace": "/home/mock/.openclaw/agents/main",
"file": {
"name": "AGENT.md",
"path": "/home/mock/.openclaw/agents/main/AGENT.md",
"missing": false,
"size": 128,
"updatedAtMs": ${datetime.now.millis},
"content": "# Main agent\nYou are the mock main agent."
}
}
}
12 changes: 12 additions & 0 deletions websocket/openclaw/agents-files-list.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"type": "res",
"id": "${stores.request.reqId}",
"ok": true,
"payload": {
"agentId": "main",
"workspace": "/home/mock/.openclaw/agents/main",
"files": [
{ "name": "AGENT.md", "path": "/home/mock/.openclaw/agents/main/AGENT.md", "missing": false, "size": 128, "updatedAtMs": ${datetime.now.millis} }
]
}
}
Loading