Deterministic local messaging-channel mocks for OpenClaw QA.
crabline is config-driven, CI-friendly, and deliberately has no openclaw
dependency. It can run fixture-level local mocks, and it can also serve local
provider APIs that OpenClaw live adapters can target during deterministic QA.
- local mock providers for
discord,feishu,googlechat,imessage,loopback,matrix,mattermost,msteams,slack,telegram,whatsapp, andzalo - a
scriptbridge for channels that are still exercised by external commands - per-provider local webhook endpoints for inbound events
- local provider servers for live-adapter smoke tests for Mattermost, Matrix, Signal, Slack, Telegram, WhatsApp, and Zalo
- JSONL recorder files for deterministic wait/watch behavior
- nonce-based
send,roundtrip,agent,probe,run,watch, anddoctorcommands - text output by default and stable
--jsonoutput for automation
Crabline local servers are not live-provider coverage. They let OpenClaw run its
normal channel adapter code against a local provider-shaped API. Release lanes
still need the live driver and real provider credentials.
pnpm install
pnpm build
pnpm verifyRun locally:
pnpm dev fixtures --config fixtures/examples/crabline.example.yaml
pnpm dev roundtrip telegram-dm --config fixtures/examples/crabline.example.yamlThe source checkout does not link its own crabline bin into
node_modules/.bin, so use pnpm dev there rather than
pnpm exec crabline. An installed package exposes the crabline command used
in the examples below.
pnpm verifyThat enforces formatting, typecheck, type-aware lint, and Vitest coverage.
Config file search order:
--config <path>./crabline.yaml./crabline.yml./crabline.json
Top-level shape:
configVersion: 1
userName: crabline
providers:
telegram:
adapter: telegram
telegram:
recorder:
path: ./.crabline/recorders/telegram.jsonl
webhook:
host: 127.0.0.1
port: 8790
path: /telegram/webhook
fixtures:
- id: telegram-dm
provider: telegram
mode: roundtrip
target:
id: "100000001"
behavior: agentProvider ids are local profile names. Fixtures reference them through
provider. Built-in adapters infer their platform from adapter; platform is
required only for adapter: script.
Most built-in provider credentials are optional mock metadata. The WhatsApp
webhook requires appSecret and verifyToken because every verification or
delivery request is authenticated. doctor checks explicit env declarations,
script command availability, and config shape without requiring live platform
credentials.
When configured, Discord publicKey, Feishu encryptKey, Google Chat endpoint
or Pub/Sub identity settings, Microsoft Teams appId, Slack signingSecret,
Telegram secretToken, and Zalo webhookSecret enforce provider-native
webhook authentication. Feishu verificationToken remains an additional or
loopback-only callback check. Microsoft Teams also requires appId for
non-loopback or explicitly public webhook endpoints.
Authenticated external webhook ingress must advertise an HTTPS publicUrl;
plain HTTP is limited to loopback-local testing.
Feishu text callbacks require valid JSON-encoded message.content; malformed
content is rejected.
All built-in mock providers support:
probesendroundtripagentwatch
The built-in providers are:
discordfeishugooglechatimessageloopbackmatrixmattermostmsteamsslacktelegramwhatsappzalo
The script adapter can bridge any OpenClaw channel represented by the
configured platform enum by running local commands for probe, send,
waitForInbound, or watch.
Treat manifests that configure adapter: script as executable, trusted code:
Crabline runs their declared commands with the configured environment. Load
them only from sources you trust and review changes before use.
serve starts provider-shaped HTTP APIs for OpenClaw live adapters. This is the
preferred Smoke CI path because OpenClaw still uses its normal channel adapter,
but the provider endpoint is local and deterministic.
Commands in this section use the installed-package form. In a source checkout,
replace crabline with pnpm dev.
serve never accepts credential values in command-line arguments. Set
CRABLINE_ACCESS_TOKEN, CRABLINE_ADMIN_TOKEN, CRABLINE_BOT_TOKEN, or
CRABLINE_SIGNING_SECRET, or pass a bounded JSON object through a file
descriptor. Package runners should use stdin:
pnpm dev --json serve slack --credentials-fd 0 < .crabline/serve-credentials.jsonThe installed CLI can also use an inherited descriptor when its launcher preserves descriptors above 2:
crabline --json serve slack --credentials-fd 3 3< .crabline/serve-credentials.jsonThe JSON fields are accessToken, adminToken, botToken, and
signingSecret. File-descriptor values override environment fallbacks. Do not
pass descriptors above 2 through package runners such as pnpm; use fd 0
instead. Keep credential files owner-readable or pipe the JSON directly from a
secret manager.
Library callers can pass onEvent to startCrablineServer, an individual
provider server, or startOpenClawCrablineAdapter. Crabline awaits the callback
after appending each API/admin event to recorderPath, so callers can react in
process while retaining the JSONL artifact as durable evidence.
Recorder files should normally have one filesystem name. If multiple processes
write through hardlinks to the same recorder inode, set
CRABLINE_RECORDER_LOCK_DIR to the same absolute writable directory for every
writer. Pre-create that directory with the ownership, group, or ACLs required
by those writers. Crabline refuses hardlinked server-recorder writes without
this shared lock namespace. The configured path must be canonical and contain
no symlink components. Scope each lock directory to one recorder filesystem;
lock identities omit device numbers so containers that mount the same inode
under different device IDs still coordinate.
Mattermost:
crabline --json serve mattermost --ready-file .crabline/mattermost-server.jsonThe JSON manifest contains:
baseUrl: OpenClawchannels.mattermost.baseUrl/MATTERMOST_URLbotToken: OpenClawchannels.mattermost.botToken/MATTERMOST_BOT_TOKENwebhookToken/MATTERMOST_TOKEN: verifies native outgoing webhook postsendpoints.websocketUrl: native Mattermost WebSocket endpointadminToken: send this as theX-Crabline-Admin-Tokenheader when posting test user messagesendpoints.adminInboundUrl: authenticated POST endpoint for inbound messagesrecorderPath: JSONL file of local provider API/admin traffic
The server implements a Mattermost API subset for text DM and channel
roundtrips, including REST authentication/status codes, WebSocket
authentication and hello, sequenced events, typing, and post
create/edit/delete events. Admin ingress is only the test control plane;
injected messages require native 26-character lowercase channel/user IDs and
are delivered to clients as channel-scoped native posted events. Optional
channelName and channelDisplayName values populate the native event fields.
POST /api/v4/posts requires a JSON media type.
Matrix:
crabline --json serve matrix --ready-file .crabline/matrix-server.jsonThe JSON manifest contains:
baseUrl: OpenClawchannels.matrix.homeserveraccessToken: OpenClawchannels.matrix.accessTokenbotUserId: OpenClawchannels.matrix.userIdendpoints.clientApiRoot: Matrix Client-Server API rootadminToken: send this as theX-Crabline-Admin-Tokenheader when posting test user messagesendpoints.adminInboundUrl: authenticated POST control endpoint for inbound messagesrecorderPath: JSONL file of local provider API/admin traffic
The server implements an unencrypted Matrix Client-Server API subset including
whoami, filters, push rules, joined rooms and members, room state, /sync,
room event sends, typing, and read receipts. Admin ingress is only the test
control plane; injected messages are delivered to clients as native
m.room.message events through /sync. Optional roomName sets newly created
room state, direct: true publishes m.direct account data, and threadId
accepts the raw Matrix root event ID used in the native thread relation.
Slack:
crabline --json serve slack --ready-file .crabline/slack-server.jsonThe JSON manifest contains:
endpoints.apiRoot: set OpenClaw's Slack API override /SLACK_API_URLto this valuebotToken: set OpenClawchannels.slack.botTokento this valuesigningSecret: set OpenClawchannels.slack.signingSecretto this valueadminToken: send this as theX-Crabline-Admin-Tokenheader when posting test user messagesendpoints.adminInboundUrl: authenticated POST endpoint for Events API-shaped test user messagesendpoints.eventsUrl: local Slack Events API endpointrecorderPath: JSONL file of local provider API/admin traffic
The admin token is generated randomly unless adminToken is supplied through
the credential ingress above. Implemented Slack Web API endpoints include auth.test,
chat.postMessage, conversations.open, conversations.info,
conversations.history, and conversations.replies.
Signal:
crabline --json serve signal --ready-file .crabline/signal-server.jsonThe JSON manifest contains:
endpoints.apiRoot:signal-cli daemon --http-compatible API rootaccount: mock Signal account served by the daemonadminToken: send this as theX-Crabline-Admin-Tokenheader when posting test user messagesendpoints.adminInboundUrl: authenticated POST endpoint for inbound messagesendpoints.eventsUrl:signal-cliSSE receive endpointendpoints.rpcUrl:signal-cliJSON-RPC endpointrecorderPath: JSONL file of local provider API/admin traffic
The server implements the HTTP surface exposed by signal-cli daemon --http:
check, events, and the JSON-RPC methods needed for text sends, typing,
receipts, and reactions. Admin ingress injects text-only receive events. It does
not replace the signal-cli client or pretend to be Signal's public service.
Telegram:
crabline --json serve telegram --ready-file .crabline/telegram-server.jsonThe JSON manifest contains:
endpoints.apiRoot: set OpenClawchannels.telegram.apiRootto this valuebotToken: set OpenClawchannels.telegram.botTokento this valueadminToken: send this as theX-Crabline-Admin-Tokenheader when posting test user messagesendpoints.adminInboundUrl: authenticated POST endpoint for test user messages; OpenClaw reads them through TelegramgetUpdatesrecorderPath: JSONL file of local provider API/admin traffic
The admin token is generated randomly unless adminToken is supplied through
the credential ingress above. The inbound endpoint rejects requests without
the matching admin header (or Authorization: Bearer <token>).
Implemented Telegram Bot API endpoints include getMe, sendMessage,
sendPhoto, sendDocument, sendVideo, sendAudio, sendAnimation, editMessageText,
deleteMessage, setMessageReaction, createForumTopic, editForumTopic,
pinChatMessage, unpinChatMessage, getUpdates, deleteWebhook,
setWebhook, setMyCommands, deleteMyCommands, sendChatAction, and
answerCallbackQuery. Webhook secrets use Telegram's 1-256 character
letter/digit/underscore/hyphen alphabet. Text and caption fields enforce native
string types and 4,096/1,024 UTF-16 limits, entity metadata is retained, and
@username method targets resolve to numeric Chat.id values.
WhatsApp:
crabline --json serve whatsapp --ready-file .crabline/whatsapp-server.jsonThe JSON manifest contains:
endpoints.apiRoot: versioned WhatsApp Graph API rootendpoints.phoneNumberUrl: provider-native phone-number resourceendpoints.baileysWebSocketUrl: Baileys-compatible WebSocket URL forwaWebSocketUrl, including the local provider access token query parameteraccessToken: bearer token for local provider requestsadminToken: send this as theX-Crabline-Admin-Tokenheader when posting test user messagesselfJid: canonical local authenticated WhatsApp user JID; legacy@c.usinput is normalized to@s.whatsapp.netenv.CLOUD_API_ACCESS_TOKEN: same value asaccessTokenenv.CLOUD_API_VERSION: Graph API version used by the local serverenv.WA_BASE_URL: local Graph API originenv.WA_PHONE_NUMBER_ID: local sender phone-number resource IDendpoints.adminInboundUrl: authenticated POST endpoint for test user messages using the WhatsApp Business webhook payload shapeendpoints.messagesUrl: provider-native Cloud API message and status endpointendpoints.statusUrl: alias for the same provider-native status endpointrecorderPath: JSONL file of local provider API/admin traffic and Baileys WebSocket stanzas
Pass endpoints.baileysWebSocketUrl to Baileys as waWebSocketUrl when a
runtime needs to connect through the local provider. Cloud API-compatible
clients can send text messages through
POST /v25.0/<phone-number-id>/messages with a bearer token. The local server completes
the Baileys Noise handshake, serves bootstrap/device/prekey queries, and records
encrypted outbound WebSocket stanzas. Direct msg and pkmsg text sends are
also recorded as normalized accepted-send evidence for the OpenClaw bridge.
Group outbound uses sender-key skmsg encryption and is outside this supported
subset, so OpenClaw Crabline outbound targets are direct users only. Group
inbound injection remains supported. The WebSocket endpoint rejects clients
that do not present the access token embedded in the manifest URL. The admin
token is generated randomly unless adminToken is supplied through the
credential ingress above.
OpenClaw bridge callers should post injected user messages with the
providerUrl, providerHeaders, and providerBody returned by
createOpenClawCrablineInbound().
Zalo:
crabline --json serve zalo --ready-file .crabline/zalo-server.jsonThe JSON manifest contains:
endpoints.apiRoot: trustedZALO_API_URLbotToken: OpenClawchannels.zalo.botToken/ZALO_BOT_TOKENbotId: local Zalo bot identityadminToken: send this as theX-Crabline-Admin-Tokenheader when posting test user messagesendpoints.adminInboundUrl: authenticated POST control endpoint for inbound messagesrecorderPath: JSONL file of local provider API/admin traffic
The server implements the Zalo Bot API subset used by OpenClaw: getMe,
single-update long polling through getUpdates, sendMessage, sendPhoto,
sendChatAction, and webhook registration, inspection, and deletion. These Bot
API methods accept GET query parameters or POST requests. Admin
ingress injects a native Zalo update into the active polling or webhook
transport. Webhook delivery posts the native { event_name, message } update
directly. OpenClaw-specific endpoint, config, and target mapping remain in the
isolated bridge.
The admin ingress accepts JSON like:
{
"chatId": "6ede9afa66b88fe6d6a9",
"chatType": "PRIVATE",
"senderId": "6ede9afa66b88fe6d6a9",
"senderName": "Alice",
"text": "user nonce-123"
}Built-in providers accept native channel identifiers. Crabline does not add
telegram:, discord:, slack:, or other local prefixes.
target:
id: "C1234567890"Thread targets use the platform's native thread identifier:
target:
id: "C1234567890"
channelId: "C1234567890"
threadId: "1700000000.000100"Examples:
- Slack conversations:
C1234567890,G1234567890, orD1234567890 - Slack direct sends may also target user IDs such as
U1234567890orW1234567890 - Slack threads:
1700000000.000100 - Telegram chats:
-1001234567890or@channelusername - Telegram topics:
42 - WhatsApp Cloud API users: digits-only
wa_idvalues such as15551234567 - WhatsApp groups:
120363001234567890@g.us - Discord channels and threads: Discord snowflake ids such as
123456789012345678 - iMessage recipients: E.164 phone numbers such as
+15551234567, email addresses such asuser@example.com, or chat GUIDs such asiMessage;-;chat-guidandSMS;+;chat-guid - Matrix rooms: scoped IDs such as
!abcdef:matrix.orgor Matrix v12 domainless room IDs; thread IDs are raw root event IDs such as$eventid:matrix.org - Mattermost channels, users, and root posts: exactly 26 lowercase alphanumeric
characters, such as
abcdefghijklmnopqrstuvwx12 - Microsoft Teams: non-empty opaque Bot Connector
conversation.idvalues such asa:opaque-conversation-idor19:conversation@thread.v2 - Zalo users, OAs, and chats: non-whitespace provider IDs such as
user-1orgroup-1
Each built-in provider starts a local webhook during probe, waitForInbound,
or watch. Webhook requests can use the provider's native event shape, or this
simple JSON shape with native thread ids:
{
"id": "slack-inbound-1",
"threadId": "C1234567890",
"text": "reply nonce-123",
"author": "assistant"
}Nested message payloads are also accepted:
{
"message": {
"id": "slack-inbound-1",
"threadId": "C1234567890",
"text": "reply nonce-123"
}
}Malformed webhooks return 400, and non-JSON requests return 415.
Configured webhook paths must be canonical URL pathnames without normalization,
query, or fragment ambiguity.
send records an outbound user event in the provider recorder. For roundtrip
and agent modes, the local mock also records a deterministic assistant reply:
[telegram mock] hello nonce-123
waitForInbound reads the recorder until it finds a matching non-user event.
watch streams matching recorder events. This gives CI channel coverage without
live service latency, external credentials, webhooks exposed to the internet, or
provider SDK state.
Recorder files remain JSONL. Individual events use one object per line; an accepted multi-message webhook is committed as one versioned batch object so readers never observe a partial batch. Crabline's recorder APIs flatten those batch objects back into their individual events.
See Channel Setup for the provider matrix, webhook paths, and OpenClaw live-vs-mock guidance.