Skip to content

Commit dff4a04

Browse files
authored
feat(signal): support container REST API
Adds container REST/WebSocket support for bbernhard/signal-cli-rest-api Signal deployments. Closes #10240. Thanks @Hua688. Verification: - pnpm exec oxfmt --check --threads=1 docs/channels/signal.md - pnpm lint:extensions - pnpm test extensions/signal - pnpm tsgo:extensions && pnpm tsgo:test:extensions - pnpm config:docs:check - git diff --check - CI checks on PR head 1d0a536 - Crabbox/Testbox live Docker smoke tbx_01kr7h07shhcafxjc0ezfh946w / run 25614453516
1 parent 7af50ce commit dff4a04

22 files changed

Lines changed: 2721 additions & 26 deletions
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Signal/container mode: add REST API support for bbernhard/signal-cli-rest-api containerized deployments via a unified adapter layer, with automatic mode detection and `channels.signal.apiMode` config. (#10240) Thanks @Hua688.
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
7317136882cafd0cfd23146218b7bec4a65a2c08328f7a1f8e22ac04789ec35d config-baseline.json
2-
8e0f397eaeaa858f016a080e2585c69fa6744cdbb0e7f68d71cfc693971dbc8c config-baseline.core.json
3-
25c6e70d5b4925e07549072159ce4fcad45813fed12fa36a2f43d3568ca8dd96 config-baseline.channel.json
4-
af8a8e8616a0146ad989ff1bc0e8cf62c61a4d434dd67bbe7fe082c5c204fada config-baseline.plugin.json
1+
31ba805dfaa665fe16735d7c2f3a5e9fb0c349aeb5e301881b5da65cee62b1f8 config-baseline.json
2+
5552e7871057e05d5699f90f536ce58e62d5b8cfc6020d3e7106be7915fed041 config-baseline.core.json
3+
80f0f51caedf14dc2138d975b62852ff7c5cf085df1c734c9de279f5859a7eeb config-baseline.channel.json
4+
3a08e5422ce1422d9e2b75feac7e44cdcd0c3dc1eea594f664bceec13cbe3f58 config-baseline.plugin.json

docs/channels/signal.md

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
---
2-
summary: "Signal support via signal-cli (JSON-RPC + SSE), setup paths, and number model"
2+
summary: "Signal support via signal-cli (native daemon or bbernhard container), setup paths, and number model"
33
read_when:
44
- Setting up Signal support
55
- Debugging Signal send/receive
66
title: "Signal"
77
---
88

9-
Status: external CLI integration. Gateway talks to `signal-cli` over HTTP JSON-RPC + SSE.
9+
Status: external CLI integration. Gateway talks to `signal-cli` over HTTP — either native daemon (JSON-RPC + SSE) or bbernhard/signal-cli-rest-api container (REST + WebSocket).
1010

1111
## Prerequisites
1212

1313
- OpenClaw installed on your server (Linux flow below tested on Ubuntu 24).
14-
- `signal-cli` available on the host where the gateway runs.
14+
- One of:
15+
- `signal-cli` available on the host (native mode), **or**
16+
- `bbernhard/signal-cli-rest-api` Docker container (container mode).
1517
- A phone number that can receive one verification SMS (for SMS registration path).
1618
- Browser access for Signal captcha (`signalcaptchas.org`) during registration.
1719

@@ -179,6 +181,63 @@ If you want to manage `signal-cli` yourself (slow JVM cold starts, container ini
179181

180182
This skips auto-spawn and the startup wait inside OpenClaw. For slow starts when auto-spawning, set `channels.signal.startupTimeoutMs`.
181183

184+
## Container mode (bbernhard/signal-cli-rest-api)
185+
186+
Instead of running `signal-cli` natively, you can use the [bbernhard/signal-cli-rest-api](https://github.com/bbernhard/signal-cli-rest-api) Docker container. This wraps `signal-cli` behind a REST API and WebSocket interface.
187+
188+
Requirements:
189+
190+
- The container **must** run with `MODE=json-rpc` for real-time message receiving.
191+
- Register or link your Signal account inside the container before connecting OpenClaw.
192+
193+
Example `docker-compose.yml` service:
194+
195+
```yaml
196+
signal-cli:
197+
image: bbernhard/signal-cli-rest-api:latest
198+
environment:
199+
MODE: json-rpc
200+
ports:
201+
- "8080:8080"
202+
volumes:
203+
- signal-cli-data:/home/.local/share/signal-cli
204+
```
205+
206+
OpenClaw config:
207+
208+
```json5
209+
{
210+
channels: {
211+
signal: {
212+
enabled: true,
213+
account: "+15551234567",
214+
httpUrl: "http://signal-cli:8080",
215+
autoStart: false,
216+
apiMode: "container", // or "auto" to detect automatically
217+
},
218+
},
219+
}
220+
```
221+
222+
The `apiMode` field controls which protocol OpenClaw uses:
223+
224+
| Value | Behavior |
225+
| ------------- | ------------------------------------------------------------------------------------ |
226+
| `"auto"` | (Default) Probes both transports; streaming validates container WebSocket receive |
227+
| `"native"` | Force native signal-cli (JSON-RPC at `/api/v1/rpc`, SSE at `/api/v1/events`) |
228+
| `"container"` | Force bbernhard container (REST at `/v2/send`, WebSocket at `/v1/receive/{account}`) |
229+
230+
When `apiMode` is `"auto"`, OpenClaw caches the detected mode for 30 seconds to avoid repeated probes. Container receive is only selected for streaming after `/v1/receive/{account}` upgrades to WebSocket, which requires `MODE=json-rpc`.
231+
232+
Container mode supports the same Signal channel operations as native mode where the container exposes matching APIs: sends, receives, attachments, typing indicators, read/viewed receipts, reactions, groups, and styled text. OpenClaw translates its native Signal RPC calls into the container's REST payloads, including `group.{base64(internal_id)}` group IDs and `text_mode: "styled"` for formatted text.
233+
234+
Operational notes:
235+
236+
- Use `autoStart: false` with container mode. OpenClaw should not spawn a native daemon when `apiMode: "container"` is selected.
237+
- Use `MODE=json-rpc` for receiving. `MODE=normal` can make `/v1/about` look healthy, but `/v1/receive/{account}` does not WebSocket-upgrade, so OpenClaw will not select container receive streaming in `auto` mode.
238+
- Set `apiMode: "container"` when you know the `httpUrl` points at bbernhard's REST API. Set `apiMode: "native"` when you know it points at native `signal-cli` JSON-RPC/SSE. Use `"auto"` when the deployment may vary.
239+
- Container attachment downloads honor the same media byte limits as native mode. Oversized responses are rejected before being fully buffered when the server sends `Content-Length`, and while streaming otherwise.
240+
182241
## Access control (DMs + groups)
183242

184243
DMs:
@@ -202,7 +261,8 @@ Groups:
202261

203262
## How it works (behavior)
204263

205-
- `signal-cli` runs as a daemon; the gateway reads events via SSE.
264+
- Native mode: `signal-cli` runs as a daemon; the gateway reads events via SSE.
265+
- Container mode: the gateway sends via REST API and receives via WebSocket.
206266
- Inbound messages are normalized into the shared channel envelope.
207267
- Replies always route back to the same number or group.
208268

@@ -302,6 +362,7 @@ Full configuration: [Configuration](/gateway/configuration)
302362
Provider options:
303363

304364
- `channels.signal.enabled`: enable/disable channel startup.
365+
- `channels.signal.apiMode`: `auto | native | container` (default: auto). See [Container mode](#container-mode-bbernhardsignal-cli-rest-api).
305366
- `channels.signal.account`: E.164 for the bot account.
306367
- `channels.signal.cliPath`: path to `signal-cli`.
307368
- `channels.signal.httpUrl`: full daemon URL (overrides host/port).

extensions/signal/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
"private": true,
55
"description": "OpenClaw Signal channel plugin",
66
"type": "module",
7+
"dependencies": {
8+
"ws": "^8.20.0"
9+
},
710
"devDependencies": {
811
"@openclaw/plugin-sdk": "workspace:*"
912
},

extensions/signal/src/channel.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,9 @@ export const signalPlugin: ChannelPlugin<ResolvedSignalAccount, SignalProbe> =
347347
probeAccount: async ({ account, timeoutMs }) => {
348348
const baseUrl = account.baseUrl;
349349
const { probeSignal } = await loadSignalProbeModule();
350-
return await probeSignal(baseUrl, timeoutMs);
350+
return await probeSignal(baseUrl, timeoutMs, {
351+
apiMode: account.config?.apiMode ?? "auto",
352+
});
351353
},
352354
formatCapabilitiesProbe: ({ probe }) =>
353355
probe?.version ? [{ text: `Signal daemon: ${probe.version}` }] : [],

0 commit comments

Comments
 (0)