diff --git a/websocket/openclaw/.imposter.yaml b/websocket/openclaw/.imposter.yaml index ffa0b15..9716830 100644 --- a/websocket/openclaw/.imposter.yaml +++ b/websocket/openclaw/.imposter.yaml @@ -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 diff --git a/websocket/openclaw/README.md b/websocket/openclaw/README.md index faa5b35..8a1a3f4 100644 --- a/websocket/openclaw/README.md +++ b/websocket/openclaw/README.md @@ -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 @@ -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 diff --git a/websocket/openclaw/agent-identity.json b/websocket/openclaw/agent-identity.json new file mode 100644 index 0000000..543457e --- /dev/null +++ b/websocket/openclaw/agent-identity.json @@ -0,0 +1,12 @@ +{ + "type": "res", + "id": "${stores.request.reqId}", + "ok": true, + "payload": { + "agentId": "main", + "name": "Main", + "emoji": "🤖", + "avatarSource": "emoji", + "avatarStatus": "emoji" + } +} diff --git a/websocket/openclaw/agents-config.yaml b/websocket/openclaw/agents-config.yaml new file mode 100644 index 0000000..f753c0e --- /dev/null +++ b/websocket/openclaw/agents-config.yaml @@ -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 diff --git a/websocket/openclaw/agents-file-get.json b/websocket/openclaw/agents-file-get.json new file mode 100644 index 0000000..63ba8ad --- /dev/null +++ b/websocket/openclaw/agents-file-get.json @@ -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." + } + } +} diff --git a/websocket/openclaw/agents-files-list.json b/websocket/openclaw/agents-files-list.json new file mode 100644 index 0000000..a99ea2b --- /dev/null +++ b/websocket/openclaw/agents-files-list.json @@ -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} } + ] + } +} diff --git a/websocket/openclaw/agents-workspace-get.json b/websocket/openclaw/agents-workspace-get.json new file mode 100644 index 0000000..716e6b5 --- /dev/null +++ b/websocket/openclaw/agents-workspace-get.json @@ -0,0 +1,17 @@ +{ + "type": "res", + "id": "${stores.request.reqId}", + "ok": true, + "payload": { + "agentId": "main", + "file": { + "path": "/home/mock/workspace/README.md", + "name": "README.md", + "size": 64, + "updatedAtMs": ${datetime.now.millis}, + "mimeType": "text/markdown", + "encoding": "utf8", + "content": "# Mock workspace\nExample file." + } + } +} diff --git a/websocket/openclaw/agents-workspace-list.json b/websocket/openclaw/agents-workspace-list.json new file mode 100644 index 0000000..8f628bb --- /dev/null +++ b/websocket/openclaw/agents-workspace-list.json @@ -0,0 +1,16 @@ +{ + "type": "res", + "id": "${stores.request.reqId}", + "ok": true, + "payload": { + "agentId": "main", + "path": "/home/mock/workspace", + "parentPath": "/home/mock", + "entries": [ + { "path": "/home/mock/workspace/README.md", "name": "README.md", "kind": "file", "size": 64, "updatedAtMs": ${datetime.now.millis} }, + { "path": "/home/mock/workspace/src", "name": "src", "kind": "directory", "updatedAtMs": ${datetime.now.millis} } + ], + "totalEntries": 2, + "offset": 0 + } +} diff --git a/websocket/openclaw/artifacts-get.json b/websocket/openclaw/artifacts-get.json new file mode 100644 index 0000000..d71cd91 --- /dev/null +++ b/websocket/openclaw/artifacts-get.json @@ -0,0 +1,17 @@ +{ + "type": "res", + "id": "${stores.request.reqId}", + "ok": true, + "payload": { + "artifact": { + "id": "artifact-1", + "type": "code", + "title": "hello.py", + "mimeType": "text/x-python", + "sizeBytes": 40, + "sessionKey": "main", + "runId": "run-1", + "download": { "mode": "bytes" } + } + } +} diff --git a/websocket/openclaw/artifacts-list.json b/websocket/openclaw/artifacts-list.json new file mode 100644 index 0000000..06f317e --- /dev/null +++ b/websocket/openclaw/artifacts-list.json @@ -0,0 +1,21 @@ +{ + "type": "res", + "id": "${stores.request.reqId}", + "ok": true, + "payload": { + "artifacts": [ + { + "id": "artifact-1", + "type": "code", + "title": "hello.py", + "mimeType": "text/x-python", + "sizeBytes": 40, + "sessionKey": "main", + "runId": "run-1", + "messageSeq": 2, + "source": "model_generation", + "download": { "mode": "bytes" } + } + ] + } +} diff --git a/websocket/openclaw/audit-activity.json b/websocket/openclaw/audit-activity.json new file mode 100644 index 0000000..313d3a2 --- /dev/null +++ b/websocket/openclaw/audit-activity.json @@ -0,0 +1,9 @@ +{ + "type": "res", + "id": "${stores.request.reqId}", + "ok": true, + "payload": { + "events": [], + "nextCursor": null + } +} diff --git a/websocket/openclaw/channels-config.yaml b/websocket/openclaw/channels-config.yaml new file mode 100644 index 0000000..b63f39a --- /dev/null +++ b/websocket/openclaw/channels-config.yaml @@ -0,0 +1,32 @@ +# OpenClaw Gateway WebSocket mock - channels domain. +# A single connected telegram account. +plugin: websocket + +resources: + # channels.status: channel and account status snapshot. + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: channels.status + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + file: channels-status.json + template: true + + # channels.logout: log a channel account out, acknowledged with ok:true. + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: channels.logout + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + content: '{"type":"res","id":"${stores.request.reqId}","ok":true}' + template: true diff --git a/websocket/openclaw/channels-status.json b/websocket/openclaw/channels-status.json new file mode 100644 index 0000000..197c76a --- /dev/null +++ b/websocket/openclaw/channels-status.json @@ -0,0 +1,27 @@ +{ + "type": "res", + "id": "${stores.request.reqId}", + "ok": true, + "payload": { + "ts": ${datetime.now.millis}, + "channelOrder": ["telegram"], + "channelLabels": { "telegram": "Telegram" }, + "channels": {}, + "channelAccounts": { + "telegram": [ + { + "accountId": "telegram-main", + "name": "Main bot", + "enabled": true, + "configured": true, + "linked": true, + "running": true, + "connected": true, + "lastConnectedAt": ${datetime.now.millis}, + "activeRuns": 0 + } + ] + }, + "channelDefaultAccountId": { "telegram": "telegram-main" } + } +} diff --git a/websocket/openclaw/chat-config.yaml b/websocket/openclaw/chat-config.yaml new file mode 100644 index 0000000..7c257c9 --- /dev/null +++ b/websocket/openclaw/chat-config.yaml @@ -0,0 +1,111 @@ +# OpenClaw Gateway WebSocket mock - chat domain. +# Transcript reads plus the streamed chat.send flow. +plugin: websocket + +resources: + # chat.history: prior messages for the session, so the chat screen renders a + # short seed conversation. Return {"messages":[]} for an empty history. + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: chat.history + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + file: chat-history.json + template: true + + # chat.message.get: fetch a single message's full content. + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: chat.message.get + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + file: chat-message-get.json + template: true + + # chat.toolTitles: batch tool-title rendering. This mock does not generate + # titles, so it reports them disabled (a valid, canonical response). + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: chat.toolTitles + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + content: '{"type":"res","id":"${stores.request.reqId}","ok":true,"payload":{"titles":{},"disabled":true}}' + template: true + + # chat.send: acknowledge with runId/status, then stream a tool agent event and + # chat delta/final events. runId is the request id; the session key is echoed + # from the request so the client routes the events to the active run. + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: chat.send + capture: + reqId: { requestBody: { jsonPath: $.id } } + sessionKey: + requestBody: + jsonPath: $.params.sessionKey + responses: + - content: '{"type":"res","id":"${stores.request.reqId}","ok":true,"payload":{"runId":"${stores.request.reqId}","status":"started"}}' + template: true + - file: agent-tool-start.json + delay: + exact: 300 + template: true + - file: agent-tool-result.json + delay: + exact: 300 + template: true + - file: chat-event-delta.json + delay: + min: 200 + max: 600 + template: true + - file: chat-event-final.json + delay: + exact: 400 + template: true + + # chat.abort / chat.inject: void RPCs, acknowledged with ok:true. + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: chat.abort + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + content: '{"type":"res","id":"${stores.request.reqId}","ok":true}' + template: true + + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: chat.inject + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + content: '{"type":"res","id":"${stores.request.reqId}","ok":true}' + template: true diff --git a/websocket/openclaw/chat-event-delta.json b/websocket/openclaw/chat-event-delta.json index cbc1f4c..8ef45dc 100644 --- a/websocket/openclaw/chat-event-delta.json +++ b/websocket/openclaw/chat-event-delta.json @@ -6,6 +6,6 @@ "sessionKey": "${stores.request.sessionKey}", "seq": 1, "state": "delta", - "message": "Here's the first part of the reply, " + "deltaText": "Here's the first part of the reply, " } } diff --git a/websocket/openclaw/chat-message-get.json b/websocket/openclaw/chat-message-get.json new file mode 100644 index 0000000..a0e7fc2 --- /dev/null +++ b/websocket/openclaw/chat-message-get.json @@ -0,0 +1,15 @@ +{ + "type": "res", + "id": "${stores.request.reqId}", + "ok": true, + "payload": { + "ok": true, + "message": { + "id": "msg-2", + "role": "assistant", + "content": [ + { "type": "text", "text": "Hi! I'm the mock main agent. Ask me anything." } + ] + } + } +} diff --git a/websocket/openclaw/commands-list.json b/websocket/openclaw/commands-list.json new file mode 100644 index 0000000..26bb52c --- /dev/null +++ b/websocket/openclaw/commands-list.json @@ -0,0 +1,34 @@ +{ + "type": "res", + "id": "${stores.request.reqId}", + "ok": true, + "payload": { + "commands": [ + { "name": "help", "description": "Show available commands", "source": "native", "scope": "both", "acceptsArgs": false, "category": "status" }, + { "name": "new", "description": "Start a new session", "source": "native", "scope": "both", "acceptsArgs": false, "category": "session" }, + { + "name": "model", + "description": "Switch the session model", + "source": "native", + "scope": "both", + "acceptsArgs": true, + "category": "options", + "args": [ + { + "name": "model", + "description": "Model id", + "type": "string", + "required": true, + "choices": [ + { "value": "claude-opus-4-8", "label": "Claude Opus 4.8" }, + { "value": "claude-sonnet-5", "label": "Claude Sonnet 5" }, + { "value": "claude-haiku-4-5", "label": "Claude Haiku 4.5" } + ] + } + ] + }, + { "name": "compact", "description": "Compact the current transcript", "source": "native", "scope": "both", "acceptsArgs": false, "category": "session" }, + { "name": "tools", "description": "List available tools", "source": "native", "scope": "both", "acceptsArgs": false, "category": "tools" } + ] + } +} diff --git a/websocket/openclaw/config-get.json b/websocket/openclaw/config-get.json new file mode 100644 index 0000000..0e8d069 --- /dev/null +++ b/websocket/openclaw/config-get.json @@ -0,0 +1,13 @@ +{ + "type": "res", + "id": "${stores.request.reqId}", + "ok": true, + "payload": { + "config": { + "gateway": { "port": 8080 }, + "agents": { "main": { "model": "claude-opus-4-8" } } + }, + "hash": "sha256:mock", + "configRevisionHash": "rev-mock-1" + } +} diff --git a/websocket/openclaw/cron-config.yaml b/websocket/openclaw/cron-config.yaml new file mode 100644 index 0000000..fe8e621 --- /dev/null +++ b/websocket/openclaw/cron-config.yaml @@ -0,0 +1,116 @@ +# OpenClaw Gateway WebSocket mock - cron domain. +# One scheduled job, 'cron-daily-digest', kept consistent across list/get/runs. +plugin: websocket + +resources: + # cron.list: one scheduled job. + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: cron.list + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + file: cron-list.json + template: true + + # cron.get: the same job by id. + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: cron.get + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + file: cron-get.json + template: true + + # cron.runs: run history for the job. + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: cron.runs + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + file: cron-runs.json + template: true + + # cron.status: scheduler status summary. + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: cron.status + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + content: '{"type":"res","id":"${stores.request.reqId}","ok":true,"payload":{"running":true,"jobs":1,"nextRunAtMs":${datetime.now.millis}}}' + template: true + + # cron.add: create a job (returns the created job). + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: cron.add + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + content: '{"type":"res","id":"${stores.request.reqId}","ok":true,"payload":{"created":true,"job":{"id":"cron-new","name":"New job","enabled":true}}}' + template: true + + # cron.update: update a job (returns the updated job). + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: cron.update + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + content: '{"type":"res","id":"${stores.request.reqId}","ok":true,"payload":{"id":"cron-daily-digest","name":"Daily digest","enabled":false}}' + template: true + + # cron.run: enqueue an immediate run. + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: cron.run + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + content: '{"type":"res","id":"${stores.request.reqId}","ok":true,"payload":{"runId":"run-cron-1","enqueued":true}}' + template: true + + # cron.remove: delete a job, acknowledged with ok:true. + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: cron.remove + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + content: '{"type":"res","id":"${stores.request.reqId}","ok":true}' + template: true diff --git a/websocket/openclaw/cron-get.json b/websocket/openclaw/cron-get.json new file mode 100644 index 0000000..1b90f90 --- /dev/null +++ b/websocket/openclaw/cron-get.json @@ -0,0 +1,18 @@ +{ + "type": "res", + "id": "${stores.request.reqId}", + "ok": true, + "payload": { + "id": "cron-daily-digest", + "name": "Daily digest", + "description": "Summarise the day's activity", + "enabled": true, + "createdAtMs": ${datetime.now.millis}, + "updatedAtMs": ${datetime.now.millis}, + "schedule": { "kind": "cron", "expr": "0 9 * * *", "tz": "UTC" }, + "sessionTarget": "isolated", + "wakeMode": "next-heartbeat", + "payload": { "kind": "agentTurn", "message": "Summarise today's activity and send me the digest.", "model": "claude-opus-4-8" }, + "state": { "nextRunAtMs": ${datetime.now.millis}, "lastRunAtMs": ${datetime.now.millis}, "lastRunStatus": "ok" } + } +} diff --git a/websocket/openclaw/cron-list.json b/websocket/openclaw/cron-list.json new file mode 100644 index 0000000..a89af2b --- /dev/null +++ b/websocket/openclaw/cron-list.json @@ -0,0 +1,26 @@ +{ + "type": "res", + "id": "${stores.request.reqId}", + "ok": true, + "payload": { + "jobs": [ + { + "id": "cron-daily-digest", + "name": "Daily digest", + "description": "Summarise the day's activity", + "enabled": true, + "createdAtMs": ${datetime.now.millis}, + "updatedAtMs": ${datetime.now.millis}, + "schedule": { "kind": "cron", "expr": "0 9 * * *", "tz": "UTC" }, + "sessionTarget": "isolated", + "wakeMode": "next-heartbeat", + "payload": { "kind": "agentTurn", "message": "Summarise today's activity and send me the digest.", "model": "claude-opus-4-8" }, + "state": { "nextRunAtMs": ${datetime.now.millis}, "lastRunAtMs": ${datetime.now.millis}, "lastRunStatus": "ok" } + } + ], + "total": 1, + "offset": 0, + "limit": 50, + "hasMore": false + } +} diff --git a/websocket/openclaw/cron-runs.json b/websocket/openclaw/cron-runs.json new file mode 100644 index 0000000..3c6965f --- /dev/null +++ b/websocket/openclaw/cron-runs.json @@ -0,0 +1,23 @@ +{ + "type": "res", + "id": "${stores.request.reqId}", + "ok": true, + "payload": { + "runs": [ + { + "ts": ${datetime.now.millis}, + "jobId": "cron-daily-digest", + "action": "finished", + "status": "ok", + "summary": "Digest sent", + "durationMs": 4200, + "model": "claude-opus-4-8", + "provider": "anthropic", + "usage": { "input_tokens": 1500, "output_tokens": 800, "total_tokens": 2300 }, + "delivered": true, + "deliveryStatus": "delivered", + "jobName": "Daily digest" + } + ] + } +} diff --git a/websocket/openclaw/hello-ok.json b/websocket/openclaw/hello-ok.json index 3cae16b..173a0ef 100644 --- a/websocket/openclaw/hello-ok.json +++ b/websocket/openclaw/hello-ok.json @@ -10,7 +10,28 @@ "connId": "${random.uuid()}" }, "features": { - "methods": ["connect", "chat.send", "chat.history", "chat.abort"], + "methods": [ + "connect", "system-info", "agent.identity.get", + "agents.list", "agents.create", "agents.update", "agents.delete", + "agents.files.list", "agents.files.get", "agents.files.set", + "agents.workspace.list", "agents.workspace.get", "agent.wait", + "sessions.list", "sessions.get", "sessions.describe", "sessions.preview", + "sessions.resolve", "sessions.usage", "sessions.create", "sessions.patch", + "sessions.reset", "sessions.delete", "sessions.abort", "sessions.compact", + "sessions.send", "sessions.steer", "sessions.subscribe", "sessions.unsubscribe", + "sessions.messages.subscribe", "sessions.messages.unsubscribe", + "sessions.groups.list", "sessions.groups.put", "sessions.groups.rename", "sessions.groups.delete", + "chat.history", "chat.message.get", "chat.send", "chat.abort", "chat.inject", "chat.toolTitles", + "models.list", "models.probe", + "cron.list", "cron.get", "cron.status", "cron.runs", "cron.add", "cron.update", "cron.run", "cron.remove", + "commands.list", "tools.catalog", "tools.effective", "tools.invoke", + "skills.status", "skills.search", "skills.detail", + "channels.status", "channels.logout", + "tasks.list", "tasks.get", "tasks.cancel", + "artifacts.list", "artifacts.get", "artifacts.download", + "audit.activity.list", "config.get", + "device.pair.list", "node.list", "environments.list", "approval.get", "exec.approval.list" + ], "events": ["connect.challenge", "tick", "chat", "agent"] }, "snapshot": { diff --git a/websocket/openclaw/imposter-config.yaml b/websocket/openclaw/imposter-config.yaml index 9889098..f45e95a 100644 --- a/websocket/openclaw/imposter-config.yaml +++ b/websocket/openclaw/imposter-config.yaml @@ -1,14 +1,16 @@ -# Simulates a subset of the OpenClaw Gateway WebSocket API: -# - on connect, the server sends a connect.challenge event and starts a periodic tick -# - a 'connect' request is answered with hello-ok, echoing the request ID -# - a 'chat.send' request is answered with a res frame (runId/status), followed by -# streamed agent (tool) and chat (delta/final) events, echoing the run's session -# - 'agents.list', 'sessions.list' and 'cron.list' requests are answered with a -# single 'main' agent, one 'main' session, and no cron jobs respectively -# - 'sessions.create' returns the 'main' session key; 'chat.history' returns a -# short seed conversation so the chat screen has something to render -# - 'models.list' returns a few models; 'sessions.patch' acknowledges setting -# the session model (or other settings) +# OpenClaw Gateway WebSocket mock - connection core. +# +# This example is split across several '*-config.yaml' files, one per protocol +# domain (sessions, chat, agents, cron, tools, channels, ...). Imposter loads +# every '*-config.yaml' in the directory, and 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. See README.md for the full method list. +# +# This file owns the connection itself: +# - on connect, send a connect.challenge event and start a periodic tick +# - answer the 'connect' handshake with hello-ok, echoing the request id +# - answer any unmocked method with a canonical method_not_found error frame plugin: websocket resources: @@ -26,7 +28,7 @@ resources: content: '{"type":"event","event":"tick","payload":{"timestamp":${datetime.now.millis}}}' template: true - # Handshake request: reply with hello-ok, echoing the request ID + # Handshake request: reply with hello-ok, echoing the request id. - path: /* requestBody: allOf: @@ -42,156 +44,19 @@ resources: file: hello-ok.json template: true - # chat.send request: acknowledge with runId/status, then stream a tool - # agent event and chat delta/final events. runId is the request id; the - # session key is echoed from the request so the client routes the events. + # Fallback for any request method this mock does not implement. It matches + # only on '$.type == req' (one condition), so every specific method resource + # - which also matches '$.method' (two conditions) - outscores it and wins. + # This only fires when no method resource matches. - path: /* requestBody: allOf: - jsonPath: $.type value: req - - jsonPath: $.method - value: chat.send - capture: - reqId: - requestBody: - jsonPath: $.id - sessionKey: - requestBody: - jsonPath: $.params.sessionKey - responses: - - content: '{"type":"res","id":"${stores.request.reqId}","ok":true,"payload":{"runId":"${stores.request.reqId}","status":"started"}}' - template: true - - file: agent-tool-start.json - delay: - exact: 300 - template: true - - file: agent-tool-result.json - delay: - exact: 300 - template: true - - file: chat-event-delta.json - delay: - min: 200 - max: 600 - template: true - - file: chat-event-final.json - delay: - exact: 400 - template: true - - # 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 - - # sessions.list: one 'main' session for the main agent - - path: /* - requestBody: - allOf: - - jsonPath: $.type - value: req - - jsonPath: $.method - value: sessions.list - capture: - reqId: - requestBody: - jsonPath: $.id - response: - content: '{"type":"res","id":"${stores.request.reqId}","ok":true,"payload":{"sessions":[{"key":"main","derivedTitle":"Main conversation","lastMessagePreview":"","updatedAt":${datetime.now.millis},"model":"claude-opus-4-8"}]}}' - template: true - - # cron.list: no scheduled jobs - - path: /* - requestBody: - allOf: - - jsonPath: $.type - value: req - - jsonPath: $.method - value: cron.list - capture: - reqId: - requestBody: - jsonPath: $.id - response: - content: '{"type":"res","id":"${stores.request.reqId}","ok":true,"payload":{"jobs":[],"total":0,"offset":0,"limit":50,"hasMore":false}}' - template: true - - # sessions.create: create or resume a session. There is a single session, - # so the created key is always 'main' - the client opens the chat on it. - - path: /* - requestBody: - allOf: - - jsonPath: $.type - value: req - - jsonPath: $.method - value: sessions.create - capture: - reqId: - requestBody: - jsonPath: $.id - response: - content: '{"type":"res","id":"${stores.request.reqId}","ok":true,"payload":{"key":"main"}}' - template: true - - # chat.history: prior messages for the session, so the chat screen renders - # a short seed conversation. Return {"messages":[]} for an empty history. - - path: /* - requestBody: - allOf: - - jsonPath: $.type - value: req - - jsonPath: $.method - value: chat.history - capture: - reqId: - requestBody: - jsonPath: $.id - response: - file: chat-history.json - template: true - - # models.list: the models the session can switch between. The 'main' session's - # current model (see sessions.list) is one of these, so the picker highlights it. - - path: /* - requestBody: - allOf: - - jsonPath: $.type - value: req - - jsonPath: $.method - value: models.list - capture: - reqId: - requestBody: - jsonPath: $.id - response: - file: models.json - template: true - - # sessions.patch: set the model (or other settings) for a session. This is a - # void RPC - the client only needs an ok:true acknowledgement. - - path: /* - requestBody: - allOf: - - jsonPath: $.type - value: req - - jsonPath: $.method - value: sessions.patch capture: reqId: requestBody: jsonPath: $.id response: - content: '{"type":"res","id":"${stores.request.reqId}","ok":true}' + content: '{"type":"res","id":"${stores.request.reqId}","ok":false,"error":{"code":"method_not_found","message":"This method is not implemented by the mock."}}' template: true diff --git a/websocket/openclaw/models-config.yaml b/websocket/openclaw/models-config.yaml new file mode 100644 index 0000000..6ae483a --- /dev/null +++ b/websocket/openclaw/models-config.yaml @@ -0,0 +1,32 @@ +# OpenClaw Gateway WebSocket mock - models domain. +plugin: websocket + +resources: + # models.list: the models the session can switch between. The 'main' session's + # current model (see sessions.list) is one of these, so the picker highlights it. + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: models.list + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + file: models.json + template: true + + # models.probe: check provider reachability. + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: models.probe + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + content: '{"type":"res","id":"${stores.request.reqId}","ok":true,"payload":{"provider":"anthropic","status":"ok","latencyMs":120,"results":[{"profileId":"default","label":"Default","status":"ok","latencyMs":120}]}}' + template: true diff --git a/websocket/openclaw/session-describe.json b/websocket/openclaw/session-describe.json new file mode 100644 index 0000000..b01bc38 --- /dev/null +++ b/websocket/openclaw/session-describe.json @@ -0,0 +1,16 @@ +{ + "type": "res", + "id": "${stores.request.reqId}", + "ok": true, + "payload": { + "key": "main", + "sessionId": "session-main", + "agentId": "main", + "derivedTitle": "Main conversation", + "lastMessagePreview": "Here's the first part of the reply, and here is the rest.", + "updatedAt": ${datetime.now.millis}, + "model": "claude-opus-4-8", + "archived": false, + "pinned": false + } +} diff --git a/websocket/openclaw/session-get.json b/websocket/openclaw/session-get.json new file mode 100644 index 0000000..e643023 --- /dev/null +++ b/websocket/openclaw/session-get.json @@ -0,0 +1,20 @@ +{ + "type": "res", + "id": "${stores.request.reqId}", + "ok": true, + "payload": { + "key": "main", + "sessionId": "session-main", + "agentId": "main", + "derivedTitle": "Main conversation", + "label": null, + "lastMessagePreview": "Here's the first part of the reply, and here is the rest.", + "updatedAt": ${datetime.now.millis}, + "createdAt": ${datetime.now.millis}, + "model": "claude-opus-4-8", + "thinkingLevel": "on", + "archived": false, + "pinned": false, + "unread": false + } +} diff --git a/websocket/openclaw/session-groups.json b/websocket/openclaw/session-groups.json new file mode 100644 index 0000000..24630f5 --- /dev/null +++ b/websocket/openclaw/session-groups.json @@ -0,0 +1,11 @@ +{ + "type": "res", + "id": "${stores.request.reqId}", + "ok": true, + "payload": { + "groups": [ + { "name": "Active", "position": 0 }, + { "name": "Archived", "position": 1 } + ] + } +} diff --git a/websocket/openclaw/session-preview.json b/websocket/openclaw/session-preview.json new file mode 100644 index 0000000..d5fa101 --- /dev/null +++ b/websocket/openclaw/session-preview.json @@ -0,0 +1,17 @@ +{ + "type": "res", + "id": "${stores.request.reqId}", + "ok": true, + "payload": { + "previews": [ + { + "key": "main", + "sessionId": "session-main", + "messages": [ + { "role": "user", "content": "Hello!", "timestamp": ${datetime.now.millis} }, + { "role": "assistant", "content": "Hi! I'm the mock main agent. Ask me anything.", "timestamp": ${datetime.now.millis} } + ] + } + ] + } +} diff --git a/websocket/openclaw/session-usage.json b/websocket/openclaw/session-usage.json new file mode 100644 index 0000000..6760dc2 --- /dev/null +++ b/websocket/openclaw/session-usage.json @@ -0,0 +1,24 @@ +{ + "type": "res", + "id": "${stores.request.reqId}", + "ok": true, + "payload": { + "sessions": [ + { + "key": "main", + "agentId": "main", + "model": "claude-opus-4-8", + "input_tokens": 1500, + "output_tokens": 800, + "total_tokens": 2300, + "costUsd": 0.0123 + } + ], + "totals": { + "input_tokens": 1500, + "output_tokens": 800, + "total_tokens": 2300, + "costUsd": 0.0123 + } + } +} diff --git a/websocket/openclaw/sessions-config.yaml b/websocket/openclaw/sessions-config.yaml new file mode 100644 index 0000000..a508971 --- /dev/null +++ b/websocket/openclaw/sessions-config.yaml @@ -0,0 +1,307 @@ +# OpenClaw Gateway WebSocket mock - sessions domain. +# Browse and manage the single 'main' session. Read methods return a +# self-consistent snapshot; write/void methods are acknowledged with ok:true. +plugin: websocket + +resources: + # sessions.list: one 'main' session for the main agent. + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: sessions.list + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + content: '{"type":"res","id":"${stores.request.reqId}","ok":true,"payload":{"sessions":[{"key":"main","derivedTitle":"Main conversation","lastMessagePreview":"","updatedAt":${datetime.now.millis},"model":"claude-opus-4-8"}]}}' + template: true + + # sessions.get: the full stored row for a session. + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: sessions.get + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + file: session-get.json + template: true + + # sessions.describe: gateway session row. + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: sessions.describe + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + file: session-describe.json + template: true + + # sessions.preview: a bounded transcript preview for the session. + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: sessions.preview + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + file: session-preview.json + template: true + + # sessions.resolve: resolve a session target to a canonical reference. + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: sessions.resolve + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + content: '{"type":"res","id":"${stores.request.reqId}","ok":true,"payload":{"key":"main","sessionId":"session-main","agentId":"main"}}' + template: true + + # sessions.usage: per-session token/cost usage summary. + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: sessions.usage + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + file: session-usage.json + template: true + + # sessions.create: create or resume a session. There is a single session, so + # the created key is always 'main' - the client opens the chat on it. + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: sessions.create + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + content: '{"type":"res","id":"${stores.request.reqId}","ok":true,"payload":{"key":"main"}}' + template: true + + # sessions.patch: set the model (or other settings) for a session. A void RPC + # - the client only needs an ok:true acknowledgement. + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: sessions.patch + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + content: '{"type":"res","id":"${stores.request.reqId}","ok":true}' + template: true + + # sessions.groups.list: custom session groups. + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: sessions.groups.list + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + file: session-groups.json + template: true + + # Void/write RPCs: reset, delete, abort, compact, send, steer, the + # subscribe/unsubscribe pairs and the group mutations. Each is acknowledged + # with an ok:true response frame, echoing the request id. + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: sessions.reset + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + content: '{"type":"res","id":"${stores.request.reqId}","ok":true}' + template: true + + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: sessions.delete + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + content: '{"type":"res","id":"${stores.request.reqId}","ok":true}' + template: true + + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: sessions.abort + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + content: '{"type":"res","id":"${stores.request.reqId}","ok":true}' + template: true + + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: sessions.compact + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + content: '{"type":"res","id":"${stores.request.reqId}","ok":true}' + template: true + + # sessions.send: like chat.send but addressed by session key; acknowledged + # with a runId/status (streamed events are shown under chat.send). + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: sessions.send + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + content: '{"type":"res","id":"${stores.request.reqId}","ok":true,"payload":{"runId":"${stores.request.reqId}","status":"started"}}' + template: true + + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: sessions.steer + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + content: '{"type":"res","id":"${stores.request.reqId}","ok":true}' + template: true + + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: sessions.subscribe + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + content: '{"type":"res","id":"${stores.request.reqId}","ok":true}' + template: true + + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: sessions.unsubscribe + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + content: '{"type":"res","id":"${stores.request.reqId}","ok":true}' + template: true + + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: sessions.messages.subscribe + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + content: '{"type":"res","id":"${stores.request.reqId}","ok":true}' + template: true + + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: sessions.messages.unsubscribe + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + content: '{"type":"res","id":"${stores.request.reqId}","ok":true}' + template: true + + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: sessions.groups.put + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + content: '{"type":"res","id":"${stores.request.reqId}","ok":true}' + template: true + + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: sessions.groups.rename + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + content: '{"type":"res","id":"${stores.request.reqId}","ok":true}' + template: true + + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: sessions.groups.delete + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + content: '{"type":"res","id":"${stores.request.reqId}","ok":true}' + template: true diff --git a/websocket/openclaw/skills-detail.json b/websocket/openclaw/skills-detail.json new file mode 100644 index 0000000..db649f2 --- /dev/null +++ b/websocket/openclaw/skills-detail.json @@ -0,0 +1,26 @@ +{ + "type": "res", + "id": "${stores.request.reqId}", + "ok": true, + "payload": { + "skill": { + "slug": "web-search", + "displayName": "Web Search", + "summary": "Search the web", + "tags": { "category": "information-retrieval" }, + "isOfficial": true, + "createdAt": ${datetime.now.millis}, + "updatedAt": ${datetime.now.millis} + }, + "latestVersion": { + "version": "1.2.0", + "createdAt": ${datetime.now.millis}, + "changelog": "Add search filters" + }, + "owner": { + "handle": "openclaw", + "displayName": "OpenClaw", + "official": true + } + } +} diff --git a/websocket/openclaw/skills-search.json b/websocket/openclaw/skills-search.json new file mode 100644 index 0000000..93235ed --- /dev/null +++ b/websocket/openclaw/skills-search.json @@ -0,0 +1,11 @@ +{ + "type": "res", + "id": "${stores.request.reqId}", + "ok": true, + "payload": { + "results": [ + { "score": 0.95, "slug": "web-search", "displayName": "Web Search", "summary": "Search the web", "version": "1.2.0", "updatedAt": ${datetime.now.millis} }, + { "score": 0.82, "slug": "github-tools", "displayName": "GitHub Tools", "summary": "Interact with GitHub repositories", "version": "2.1.0", "updatedAt": ${datetime.now.millis} } + ] + } +} diff --git a/websocket/openclaw/skills-status.json b/websocket/openclaw/skills-status.json new file mode 100644 index 0000000..093c38b --- /dev/null +++ b/websocket/openclaw/skills-status.json @@ -0,0 +1,11 @@ +{ + "type": "res", + "id": "${stores.request.reqId}", + "ok": true, + "payload": { + "skills": [ + { "slug": "web-search", "displayName": "Web Search", "enabled": true, "version": "1.2.0", "source": "clawhub" }, + { "slug": "github-tools", "displayName": "GitHub Tools", "enabled": false, "version": "2.1.0", "source": "clawhub" } + ] + } +} diff --git a/websocket/openclaw/system-config.yaml b/websocket/openclaw/system-config.yaml new file mode 100644 index 0000000..96b4d5a --- /dev/null +++ b/websocket/openclaw/system-config.yaml @@ -0,0 +1,117 @@ +# OpenClaw Gateway WebSocket mock - system, audit, config and pairing. +# On a simple single-node local gateway the pairing/node/environment/approval +# lists are legitimately empty. +plugin: websocket + +resources: + # system-info: gateway host identity and resource snapshot. + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: system-info + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + file: system-info.json + template: true + + # audit.activity.list: metadata-only activity ledger (empty in this mock). + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: audit.activity.list + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + file: audit-activity.json + template: true + + # config.get: the on-disk config snapshot with its hash. + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: config.get + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + file: config-get.json + template: true + + # device.pair.list: no pending or approved devices. + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: device.pair.list + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + content: '{"type":"res","id":"${stores.request.reqId}","ok":true,"payload":{"pending":[],"approved":[]}}' + template: true + + # node.list: no companion nodes. + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: node.list + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + content: '{"type":"res","id":"${stores.request.reqId}","ok":true,"payload":{"nodes":[]}}' + template: true + + # environments.list: no worker environments. + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: environments.list + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + content: '{"type":"res","id":"${stores.request.reqId}","ok":true,"payload":{"environments":[]}}' + template: true + + # exec.approval.list: no pending exec approvals. + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: exec.approval.list + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + content: '{"type":"res","id":"${stores.request.reqId}","ok":true,"payload":{"approvals":[]}}' + template: true + + # approval.get: no such approval (nothing pending). + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: approval.get + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + content: '{"type":"res","id":"${stores.request.reqId}","ok":true,"payload":{"approval":null}}' + template: true diff --git a/websocket/openclaw/system-info.json b/websocket/openclaw/system-info.json new file mode 100644 index 0000000..ebf1b5a --- /dev/null +++ b/websocket/openclaw/system-info.json @@ -0,0 +1,27 @@ +{ + "type": "res", + "id": "${stores.request.reqId}", + "ok": true, + "payload": { + "machineName": "mock-gateway", + "hostname": "mock-gateway.local", + "platform": "darwin", + "release": "25.5.0", + "arch": "arm64", + "osLabel": "macOS 15.5", + "lanAddress": "192.168.1.42", + "port": 8080, + "nodeVersion": "mock", + "pid": 1234, + "processInstanceId": "proc-mock-1", + "uptimeMs": 3600000, + "cpuCount": 8, + "cpuModel": "Apple M3", + "loadAverage": [1.2, 1.1, 0.9], + "memoryTotalBytes": 34359738368, + "memoryFreeBytes": 8589934592, + "diskTotalBytes": 1099511627776, + "diskAvailableBytes": 549755813888, + "diskPath": "/" + } +} diff --git a/websocket/openclaw/tasks-config.yaml b/websocket/openclaw/tasks-config.yaml new file mode 100644 index 0000000..db62187 --- /dev/null +++ b/websocket/openclaw/tasks-config.yaml @@ -0,0 +1,88 @@ +# OpenClaw Gateway WebSocket mock - tasks and artifacts. +# One completed task and one code artifact, kept consistent across methods. +plugin: websocket + +resources: + # tasks.list: the task ledger. + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: tasks.list + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + file: tasks-list.json + template: true + + # tasks.get: one task by id. + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: tasks.get + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + file: tasks-get.json + template: true + + # tasks.cancel: cancel a task, acknowledged with the result. + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: tasks.cancel + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + content: '{"type":"res","id":"${stores.request.reqId}","ok":true,"payload":{"found":true,"cancelled":true,"task":{"id":"task-1","status":"cancelled"}}}' + template: true + + # artifacts.list: artifacts produced in the session. + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: artifacts.list + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + file: artifacts-list.json + template: true + + # artifacts.get: one artifact's metadata. + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: artifacts.get + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + file: artifacts-get.json + template: true + + # artifacts.download: base64 bytes for a downloadable artifact. + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: artifacts.download + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + content: '{"type":"res","id":"${stores.request.reqId}","ok":true,"payload":{"artifact":{"id":"artifact-1","type":"code","title":"hello.py","mimeType":"text/x-python","sizeBytes":40,"download":{"mode":"bytes"}},"encoding":"base64","data":"cHJpbnQoImhlbGxvIGZyb20gdGhlIG1vY2sgbWFpbiBhZ2VudCIpCg=="}}' + template: true diff --git a/websocket/openclaw/tasks-get.json b/websocket/openclaw/tasks-get.json new file mode 100644 index 0000000..d62a52d --- /dev/null +++ b/websocket/openclaw/tasks-get.json @@ -0,0 +1,24 @@ +{ + "type": "res", + "id": "${stores.request.reqId}", + "ok": true, + "payload": { + "task": { + "id": "task-1", + "kind": "agentTurn", + "runtime": "agent", + "status": "completed", + "title": "Summarise the README", + "agentId": "main", + "sessionKey": "main", + "runId": "run-1", + "createdAt": ${datetime.now.millis}, + "updatedAt": ${datetime.now.millis}, + "startedAt": ${datetime.now.millis}, + "endedAt": ${datetime.now.millis}, + "toolUseCount": 2, + "lastToolName": "read_file", + "terminalSummary": "Summarised the project README." + } + } +} diff --git a/websocket/openclaw/tasks-list.json b/websocket/openclaw/tasks-list.json new file mode 100644 index 0000000..97fd6f3 --- /dev/null +++ b/websocket/openclaw/tasks-list.json @@ -0,0 +1,27 @@ +{ + "type": "res", + "id": "${stores.request.reqId}", + "ok": true, + "payload": { + "tasks": [ + { + "id": "task-1", + "kind": "agentTurn", + "runtime": "agent", + "status": "completed", + "title": "Summarise the README", + "agentId": "main", + "sessionKey": "main", + "runId": "run-1", + "createdAt": ${datetime.now.millis}, + "updatedAt": ${datetime.now.millis}, + "startedAt": ${datetime.now.millis}, + "endedAt": ${datetime.now.millis}, + "toolUseCount": 2, + "lastToolName": "read_file", + "terminalSummary": "Summarised the project README." + } + ], + "nextCursor": null + } +} diff --git a/websocket/openclaw/tools-catalog.json b/websocket/openclaw/tools-catalog.json new file mode 100644 index 0000000..19467d9 --- /dev/null +++ b/websocket/openclaw/tools-catalog.json @@ -0,0 +1,25 @@ +{ + "type": "res", + "id": "${stores.request.reqId}", + "ok": true, + "payload": { + "agentId": "main", + "profiles": [ + { "id": "minimal", "label": "Minimal" }, + { "id": "coding", "label": "Coding" }, + { "id": "messaging", "label": "Messaging" }, + { "id": "full", "label": "Full" } + ], + "groups": [ + { + "id": "core", + "label": "Core Tools", + "tools": [ + { "id": "message", "label": "Send message", "description": "Send a message to a channel", "source": "core", "riskLevel": "low", "tags": ["messaging"] }, + { "id": "read_file", "label": "Read file", "description": "Read a file from the workspace", "source": "core", "riskLevel": "low", "tags": ["files"] }, + { "id": "exec", "label": "Execute command", "description": "Run a shell command", "source": "core", "riskLevel": "high", "tags": ["system"] } + ] + } + ] + } +} diff --git a/websocket/openclaw/tools-config.yaml b/websocket/openclaw/tools-config.yaml new file mode 100644 index 0000000..5ebfb28 --- /dev/null +++ b/websocket/openclaw/tools-config.yaml @@ -0,0 +1,101 @@ +# OpenClaw Gateway WebSocket mock - commands, tools and skills. +plugin: websocket + +resources: + # commands.list: the command inventory (native + skill commands). + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: commands.list + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + file: commands-list.json + template: true + + # tools.catalog: all runtime tools grouped by source. + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: tools.catalog + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + file: tools-catalog.json + template: true + + # tools.effective: tools effective for the 'main' session. + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: tools.effective + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + file: tools-effective.json + template: true + + # tools.invoke: run a tool directly (returns an SDK-style envelope). + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: tools.invoke + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + content: '{"type":"res","id":"${stores.request.reqId}","ok":true,"payload":{"ok":true,"toolName":"read_file","output":{"bytes":40},"source":"core"}}' + template: true + + # skills.status: installed skills for the main agent. + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: skills.status + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + file: skills-status.json + template: true + + # skills.search: ClawHub skill discovery. + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: skills.search + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + file: skills-search.json + template: true + + # skills.detail: detail for one skill. + - path: /* + requestBody: + allOf: + - jsonPath: $.type + value: req + - jsonPath: $.method + value: skills.detail + capture: + reqId: { requestBody: { jsonPath: $.id } } + response: + file: skills-detail.json + template: true diff --git a/websocket/openclaw/tools-effective.json b/websocket/openclaw/tools-effective.json new file mode 100644 index 0000000..b3c13bc --- /dev/null +++ b/websocket/openclaw/tools-effective.json @@ -0,0 +1,24 @@ +{ + "type": "res", + "id": "${stores.request.reqId}", + "ok": true, + "payload": { + "agentId": "main", + "profile": "full", + "groups": [ + { + "id": "core", + "label": "Core Tools", + "source": "core", + "tools": [ + { "id": "message", "label": "Send message", "description": "Send a message to a channel", "rawDescription": "Send a message to a channel", "source": "core", "risk": "low", "tags": ["messaging"] }, + { "id": "read_file", "label": "Read file", "description": "Read a file from the workspace", "rawDescription": "Read a file from the workspace", "source": "core", "risk": "low", "tags": ["files"] }, + { "id": "exec", "label": "Execute command", "description": "Run a shell command", "rawDescription": "Run a shell command", "source": "core", "risk": "high", "tags": ["system"] } + ] + } + ], + "notices": [ + { "severity": "info", "message": "High-risk tools require approval before use." } + ] + } +}