Skip to content

Signal: add REST API support for containerized deployments#16085

Merged
steipete merged 11 commits into
openclaw:mainfrom
Hua688:feature/signal-bridge-v2-signal-only
May 10, 2026
Merged

Signal: add REST API support for containerized deployments#16085
steipete merged 11 commits into
openclaw:mainfrom
Hua688:feature/signal-bridge-v2-signal-only

Conversation

@Hua688

@Hua688 Hua688 commented Feb 14, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds Signal support for bbernhard/signal-cli-rest-api container deployments through a container REST/WebSocket client and a native/container adapter.
  • Adds channels.signal.apiMode with auto, native, and container modes; auto mode probes the active configured endpoint and caches the selected transport.
  • Preserves native signal-cli behavior while routing container sends, receives, attachments, typing indicators, receipts, reactions, groups, and styled text through the bbernhard API contracts.
  • Updates Signal docs with container-mode setup notes, receive-mode requirements, endpoint behavior, and troubleshooting guidance.

Linked Issue/PR

User-visible / Behavior Changes

  • Signal can now run against the bbernhard/signal-cli-rest-api Docker container instead of requiring host-installed signal-cli.
  • Container mode uses REST endpoints for one-shot operations and WebSocket receive streaming when the container is configured for receive support.
  • Users can force channels.signal.apiMode to native or container, or keep the default auto detection.

Security Impact

  • New permissions/capabilities? No.
  • Secrets/tokens handling changed? No.
  • New/changed network calls? Yes, container mode calls the same configured channels.signal.httpUrl using bbernhard endpoints such as /v1/about, /v2/send, /v1/receive/{account}, /v1/typing-indicator, /v1/receipts, and /v1/reactions.
  • Command/tool execution surface changed? No.
  • Data access scope changed? No.
  • Risk + mitigation: Same trust boundary as native mode; all calls stay on the user-configured Signal endpoint. Attachment download paths enforce configured byte limits.

Compatibility / Migration

  • Backward compatible? Yes.
  • Config/env changes? Yes, new optional channels.signal.apiMode field with default auto.
  • Migration needed? No.
  • Recovery: set channels.signal.apiMode to native to force the previous native signal-cli path.

Real behavior proof

  • Behavior or issue addressed: Signal container mode support for bbernhard/signal-cli-rest-api, including the after-fix auto-detection behavior that must not select a container receive stream unless /v1/receive/{account} WebSocket-upgrades.
  • Real environment tested: Crabbox / Blacksmith Testbox Linux runner with Docker 28.0.4 running bbernhard/signal-cli-rest-api:latest in MODE=normal on 127.0.0.1:18080, plus OpenClaw's Signal container client from this PR branch.
  • Exact steps or command run after this patch: Started the Docker container with docker run -d --name signal-rest-normal -p 18080:8080 -e MODE=normal bbernhard/signal-cli-rest-api:latest, waited for curl -fsS http://127.0.0.1:18080/v1/about, then ran pnpm exec tsx to import extensions/signal/src/client-container.ts and call containerCheck("http://127.0.0.1:18080", 3000) and containerCheck("http://127.0.0.1:18080", 3000, "+15550001111").
  • Evidence after fix: Terminal output from Crabbox / Blacksmith Testbox tbx_01kr7h07shhcafxjc0ezfh946w, GitHub Actions run 25614453516. Copied live output: /v1/about returned {"versions":["v1","v2"],"build":2,"mode":"normal","version":"0.99","capabilities":{"v2/send":["quotes","mentions"]}}. OpenClaw client output included REST_ONLY {"ok":true,"status":200,"error":null} and RECEIVE_CHECK {"ok":false,"status":400,"error":"Signal container receive endpoint did not upgrade to WebSocket (HTTP 400)"}. Focused after-fix Crabbox Signal tests also passed on Testbox tbx_01kr7s89k99eq3pps18qxv3a5e: 5 files, 117 tests.
  • Observed result after fix: The live bbernhard container REST API was accepted as reachable, while the non-WebSocket MODE=normal receive endpoint was rejected for streaming. This confirms auto mode will not enter the reconnect loop against a container receive endpoint that does not upgrade, and explicit container/native mode selection remains covered by the focused Signal tests.
  • What was not tested: Full Signal account registration/linking and real phone-to-phone message delivery were not tested in this PR validation; the live proof used the real container API endpoints available without a registered Signal account.

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

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

13 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment thread src/signal/client-adapter.ts Outdated
Comment on lines +101 to +106
case "send": {
const recipients = (params.recipient as string[] | undefined) ?? [];
const groupId = params.groupId as string | undefined;
const formattedGroupId = groupId ? formatGroupIdForContainer(groupId) : undefined;
const finalRecipients =
recipients.length > 0 ? recipients : formattedGroupId ? [formattedGroupId] : [];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Container send ignores username targets

The "send" case only reads params.recipient and params.groupId, but sendMessageSignal in send.ts can also produce params.username (via buildTargetParams for username-type targets). When a username target is used in container mode, recipients will be empty and groupId will be undefined, resulting in finalRecipients = [] — the message is silently sent to nobody.

Consider handling the username param (or throwing an explicit error if the bbernhard container doesn't support username-based sends):

Suggested change
case "send": {
const recipients = (params.recipient as string[] | undefined) ?? [];
const groupId = params.groupId as string | undefined;
const formattedGroupId = groupId ? formatGroupIdForContainer(groupId) : undefined;
const finalRecipients =
recipients.length > 0 ? recipients : formattedGroupId ? [formattedGroupId] : [];
const recipients = (params.recipient as string[] | undefined) ?? [];
const usernames = (params.username as string[] | undefined) ?? [];
const groupId = params.groupId as string | undefined;
const formattedGroupId = groupId ? formatGroupIdForContainer(groupId) : undefined;
const finalRecipients =
recipients.length > 0 ? recipients : usernames.length > 0 ? usernames : formattedGroupId ? [formattedGroupId] : [];
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/signal/client-adapter.ts
Line: 101:106

Comment:
**Container send ignores `username` targets**

The `"send"` case only reads `params.recipient` and `params.groupId`, but `sendMessageSignal` in `send.ts` can also produce `params.username` (via `buildTargetParams` for username-type targets). When a username target is used in container mode, `recipients` will be empty and `groupId` will be undefined, resulting in `finalRecipients = []` — the message is silently sent to nobody.

Consider handling the `username` param (or throwing an explicit error if the bbernhard container doesn't support username-based sends):

```suggestion
      const recipients = (params.recipient as string[] | undefined) ?? [];
      const usernames = (params.username as string[] | undefined) ?? [];
      const groupId = params.groupId as string | undefined;
      const formattedGroupId = groupId ? formatGroupIdForContainer(groupId) : undefined;
      const finalRecipients =
        recipients.length > 0 ? recipients : usernames.length > 0 ? usernames : formattedGroupId ? [formattedGroupId] : [];
```

How can I resolve this? If you propose a fix, please make it concise.

@greptile-apps

greptile-apps Bot commented Feb 14, 2026

Copy link
Copy Markdown
Contributor
Additional Comments (1)

src/signal/monitor.tool-result.pairs-uuid-only-senders-uuid-allowlist-entry.e2e.test.ts
Missing adapter/container mocks will cause test hang

monitor.ts now imports adapterRpcRequest and checkAdapter from ./client-adapter.js, and uses runSignalSseLoop (which calls streamSignalEventsAdapter). The adapter's resolveApiMode defaults to "auto" when apiMode isn't in the config, which triggers detectSignalApiMode — this races signalCheck (mocked here to return {} with falsy ok) against containerCheck (unmocked, making a real HTTP request). Both probes fail, causing Promise.any to throw, and runSignalSseLoop enters an infinite reconnect loop.

The other e2e test (sends-tool-summaries-responseprefix) correctly handles this by mocking both ./client-adapter.js and ./sse-reconnect.js. This test should do the same, or alternatively set apiMode: "native" in the test config to skip auto-detection:

config = {
  channels: {
    signal: { autoStart: false, dmPolicy: "open", allowFrom: ["*"], apiMode: "native" },
  },
};
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/signal/monitor.tool-result.pairs-uuid-only-senders-uuid-allowlist-entry.e2e.test.ts
Line: 47:51

Comment:
**Missing adapter/container mocks will cause test hang**

`monitor.ts` now imports `adapterRpcRequest` and `checkAdapter` from `./client-adapter.js`, and uses `runSignalSseLoop` (which calls `streamSignalEventsAdapter`). The adapter's `resolveApiMode` defaults to `"auto"` when `apiMode` isn't in the config, which triggers `detectSignalApiMode` — this races `signalCheck` (mocked here to return `{}` with falsy `ok`) against `containerCheck` (unmocked, making a real HTTP request). Both probes fail, causing `Promise.any` to throw, and `runSignalSseLoop` enters an infinite reconnect loop.

The other e2e test (`sends-tool-summaries-responseprefix`) correctly handles this by mocking both `./client-adapter.js` and `./sse-reconnect.js`. This test should do the same, or alternatively set `apiMode: "native"` in the test config to skip auto-detection:

```
config = {
  channels: {
    signal: { autoStart: false, dmPolicy: "open", allowFrom: ["*"], apiMode: "native" },
  },
};
```

How can I resolve this? If you propose a fix, please make it concise.

@Hua688

Hua688 commented Feb 14, 2026

Copy link
Copy Markdown
Contributor Author

Updated (cleaned formatting):

Addressed both review points in d59c7ff46:

  1. src/signal/monitor.tool-result.pairs-uuid-only-senders-uuid-allowlist-entry.e2e.test.ts

    • Added apiMode: "native" in test config to avoid auto-detection probe/reconnect behavior.
  2. src/signal/client-adapter.ts

    • Container send routing now handles username targets with fallback order:
      • recipient -> username -> groupId
    • Added a unit test in src/signal/client-adapter.test.ts.

Validation:

  • pnpm test src/signal
  • pnpm check

@Hua688

Hua688 commented Feb 14, 2026

Copy link
Copy Markdown
Contributor Author

Updated with concise summary:

  • Fixed container send routing to handle username targets (recipient -> username -> groupId).
  • Added targeted test coverage in src/signal/client-adapter.test.ts.
  • Set apiMode: "native" in monitor.tool-result.pairs-uuid-only-senders-uuid-allowlist-entry.e2e.test.ts to avoid auto-detect/reconnect behavior in test runs.

Validation:

  • pnpm test src/signal → 12 files, 144 tests passed
  • pnpm check passed

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

13 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

Comment thread src/config/types.signal.ts Outdated
* - "native": Use native signal-cli with JSON-RPC + SSE (/api/v1/rpc, /api/v1/events)
* - "container": Use bbernhard/signal-cli-rest-api with REST + WebSocket (/v2/send, /v1/receive/{account})
*/
apiMode?: SignalApiMode;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

apiMode defined at account level but only read from channel-global config

The apiMode field is defined in SignalAccountConfig (line 35), but client-adapter.ts:44 reads it exclusively from cfg.channels?.signal?.apiMode. Users might configure apiMode per account expecting it to work, but it will be ignored. Either remove it from SignalAccountConfig or update the implementation to respect account-level overrides.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/config/types.signal.ts
Line: 35:35

Comment:
`apiMode` defined at account level but only read from channel-global config

The `apiMode` field is defined in `SignalAccountConfig` (line 35), but `client-adapter.ts:44` reads it exclusively from `cfg.channels?.signal?.apiMode`. Users might configure `apiMode` per account expecting it to work, but it will be ignored. Either remove it from `SignalAccountConfig` or update the implementation to respect account-level overrides.

How can I resolve this? If you propose a fix, please make it concise.

Comment thread src/signal/client-adapter.ts Outdated
};

// Cache auto-detected modes per baseUrl to avoid repeated network probes.
const detectedModeCache = new Map<string, "native" | "container">();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Module-scoped cache never invalidated

The detectedModeCache persists for the lifetime of the process with no invalidation mechanism. If the Signal service configuration changes (e.g., switching from native to container) or if apiMode config is updated at runtime, the cache will serve stale values. Consider adding a cache invalidation method or TTL.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/signal/client-adapter.ts
Line: 32:32

Comment:
Module-scoped cache never invalidated

The `detectedModeCache` persists for the lifetime of the process with no invalidation mechanism. If the Signal service configuration changes (e.g., switching from native to container) or if `apiMode` config is updated at runtime, the cache will serve stale values. Consider adding a cache invalidation method or TTL.

How can I resolve this? If you propose a fix, please make it concise.

Comment thread src/signal/client-adapter.ts Outdated
Comment on lines +164 to +172
const groupId = (params.groupIds as string[] | undefined)?.[0] ?? undefined;
const reactionParams = {
baseUrl: opts.baseUrl,
account: (params.account as string) ?? "",
recipient,
emoji: (params.emoji as string) ?? "",
targetAuthor: (params.targetAuthor as string) ?? recipient,
targetTimestamp: params.targetTimestamp as number,
groupId,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check if group ID needs formatting for container reactions

In the "send" case (lines 105-113), group IDs are formatted via formatGroupIdForContainer before being passed to the container. However, here in sendReaction, the groupId on line 172 is passed directly without formatting. Verify the bbernhard container's /v1/reactions endpoint expects the same group.{base64} format for consistency.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/signal/client-adapter.ts
Line: 164:172

Comment:
Check if group ID needs formatting for container reactions

In the `"send"` case (lines 105-113), group IDs are formatted via `formatGroupIdForContainer` before being passed to the container. However, here in `sendReaction`, the `groupId` on line 172 is passed directly without formatting. Verify the bbernhard container's `/v1/reactions` endpoint expects the same `group.{base64}` format for consistency.

How can I resolve this? If you propose a fix, please make it concise.

@Hua688
Hua688 force-pushed the feature/signal-bridge-v2-signal-only branch from d59c7ff to 3824ab5 Compare February 17, 2026 08:14
@Hua688

Hua688 commented Feb 17, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto latest upstream/main and force-pushed.

Addressed greptile follow-ups:

  • apiMode is now channel-global in config types (removed from account-level shape).
  • Added TTL-based invalidation for auto-detected adapter mode cache.
  • Normalized container reaction groupId through the same group formatter used by send path.

Validation:

  • pnpm test src/signal
  • pnpm check still fails in this branch due existing repo-wide format baseline (Format issues found in above 1118 files), unchanged by this PR.

@Hua688
Hua688 force-pushed the feature/signal-bridge-v2-signal-only branch 5 times, most recently from 3175099 to 4d750d0 Compare February 19, 2026 17:34
@openclaw-barnacle openclaw-barnacle Bot added the app: ios App: ios label Feb 19, 2026
@Hua688
Hua688 force-pushed the feature/signal-bridge-v2-signal-only branch from 4d750d0 to b30b6a0 Compare February 19, 2026 17:39
@openclaw-barnacle openclaw-barnacle Bot removed the app: ios App: ios label Feb 19, 2026
@Hua688
Hua688 force-pushed the feature/signal-bridge-v2-signal-only branch from b30b6a0 to 60d1e19 Compare February 21, 2026 12:23
@openclaw-barnacle openclaw-barnacle Bot added the docs Improvements or additions to documentation label Feb 21, 2026
@Hua688
Hua688 force-pushed the feature/signal-bridge-v2-signal-only branch 3 times, most recently from a4cb32c to 1ce3b01 Compare February 21, 2026 13:11
@openclaw-barnacle openclaw-barnacle Bot removed the docs Improvements or additions to documentation label Feb 21, 2026
@Hua688 Hua688 changed the title Signal: encapsulate native/container mode in adapter Signal: fix container outbound attachments and add native/container adapter Feb 21, 2026
@Hua688 Hua688 changed the title Signal: fix container outbound attachments and add native/container adapter Signal: add REST API support for containerized deployments Feb 21, 2026
@Hua688
Hua688 force-pushed the feature/signal-bridge-v2-signal-only branch from 0a3e66b to 0ebf2e5 Compare February 21, 2026 13:59
@openclaw-barnacle openclaw-barnacle Bot removed the stale Marked as stale due to inactivity label Apr 14, 2026
@Hua688
Hua688 force-pushed the feature/signal-bridge-v2-signal-only branch from 58b06e5 to 5a3b0ae Compare April 15, 2026 10:15
@openclaw-barnacle

Copy link
Copy Markdown

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@steipete

steipete commented Apr 26, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge.

What this changes:

This PR adds a Signal native/container adapter, a bbernhard signal-cli-rest-api REST/WebSocket client, channels.signal.apiMode config/docs/tests, and a ws runtime dependency.

Maintainer follow-up before merge:

This is an open implementation PR with new transport and dependency behavior, raw payload logging, and missing live container proof, so the next action is maintainer review/update of the existing branch rather than an autonomous replacement fix PR.

Best possible solution:

Ship a plugin-local Signal transport adapter that preserves native signal-cli JSON-RPC/SSE behavior while adding documented bbernhard REST/WebSocket mode, with aligned config schema/help/docs, targeted tests, and live container proof.

Acceptance criteria:

  • pnpm test extensions/signal/src/client.test.ts extensions/signal/src/client-adapter.test.ts extensions/signal/src/client-container.test.ts extensions/signal/src/monitor.tool-result.pairs-uuid-only-senders-uuid-allowlist-entry.test.ts
  • pnpm test:extension signal
  • pnpm check:changed in Testbox before handoff if the PR branch is updated
  • Live bbernhard/signal-cli-rest-api container smoke for send, receive, attachment, group, reaction, and receipt paths, or explicit maintainer waiver

What I checked:

  • Current main is native RPC/SSE only: signalRpcRequest posts to /api/v1/rpc, signalCheck probes /api/v1/check, and streamSignalEvents opens /api/v1/events; there is no REST /v2/send or WebSocket /v1/receive/{account} path on main. (extensions/signal/src/client.ts:189, 542821cd1e60)
  • Current callers still import native client: Signal monitor, send, reactions, probe, and reconnect paths import from ./client.js; a search for client-adapter, client-container, apiMode, /v2/send, and /v1/receive across the Signal plugin/config/docs returned no current-main hits. (extensions/signal/src/monitor.ts:37, 542821cd1e60)
  • Current config surface lacks apiMode: Signal config types and the Zod schema include account/http/autostart/reaction fields but no SignalApiMode or channels.signal.apiMode. (src/config/zod-schema.providers-core.ts:1072, 542821cd1e60)
  • Docs still describe native signal-cli: The public Signal docs currently say the gateway talks to signal-cli over HTTP JSON-RPC + SSE and document only host-installed/external daemon native setup, not container apiMode. Public docs: docs/channels/signal.md. (docs/channels/signal.md:9, 542821cd1e60)
  • PR targets real upstream container endpoints: Upstream bbernhard examples document sending through POST /v2/send with base64_attachments and receiving through /v1/receive/<number> as a WebSocket connection, matching the PR's intended transport split. (github.com)
  • Security review surface: The PR diff adds a ws dependency and a new WebSocket client. The diff is purpose-aligned, but client-container.ts logs the first 200 characters of each raw WebSocket message via the runtime logger, which can expose Signal message content or sender metadata unless removed or gated. (extensions/signal/src/client-container.ts:202, b110a8e468b5)

Likely related people:

  • steipete: Recent GitHub history shows repeated maintenance on Signal runtime paths and plugin-sdk runtime/config seams, and steipete posted the maintainer review keeping this PR open as the canonical container support candidate. (role: recent Signal runtime maintainer and reviewer; confidence: high; commits: f0000ab72d01, 4336a7f3a9c6, 7f3f108521f4; files: extensions/signal/src/client.ts, extensions/signal/src/monitor.ts, extensions/signal/src/send.ts)
  • vincentkoc: Recent history shows Signal docs and channel configuration documentation maintenance, plus dependency-hardening work that touched the Signal client path; this PR needs docs/config alignment before merge. (role: recent Signal docs/config-adjacent maintainer; confidence: medium; commits: f4129cdd2ba8, 1042b893f617, f051204bea1e; files: docs/channels/signal.md, docs/gateway/config-channels.md, extensions/signal/src/client.ts)
  • scoootscooob: Commit history shows the Signal implementation was moved into extensions/signal/src, which is relevant to reviewing this PR's plugin-boundary changes. (role: Signal extension migration owner; confidence: medium; commits: 4540c6b3bc12; files: extensions/signal/src/client.ts, extensions/signal/src/monitor.ts, extensions/signal/src/send.ts)

Remaining risk / open question:

  • The PR branch logs raw inbound WebSocket payload snippets through the runtime logger; that can expose Signal message text or sender metadata unless removed or gated before merge.
  • The PR body says live containerized deployment was not verified, so send, receive, attachment, group, reaction, and receipt paths still need bbernhard container smoke proof or an explicit maintainer waiver.
  • The new ws runtime dependency and auto-detection/cache behavior are security- and supply-chain-relevant enough to need maintainer review rather than automated cleanup.

Codex review notes: model gpt-5.5, reasoning high; reviewed against 542821cd1e60.

@Hua688

Hua688 commented Apr 26, 2026

Copy link
Copy Markdown
Contributor Author

Updated to narrow the scope of this PR.

I removed the Signal status reaction behavior from this branch for now, so #16085 focuses only on container REST/WebSocket support for bbernhard/signal-cli-rest-api:

  • keeps the adapter/container client, apiMode, REST /v2/send, and WebSocket /v1/receive/{account} support
  • removes the automatic queued/thinking/tool/compacting status reaction lifecycle changes from monitor/event-handler.ts
  • declares the ws runtime dependency required by the container WebSocket receive path

The status reaction work can be handled separately after this container support PR is reviewed.

@clawsweeper

clawsweeper Bot commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs real behavior proof before merge.

Summary
The PR adds a Signal adapter, container REST/WebSocket client, channels.signal.apiMode config/schema/docs, tests, changelog fragment, and a plugin-local ws dependency for bbernhard/signal-cli-rest-api deployments.

Reproducibility: yes. source inspection gives a high-confidence reproduction path: current main always calls native /api/v1/rpc, /api/v1/check, and /api/v1/events, while bbernhard container docs expose /v2/send and /v1/receive/{number}. I did not run a live container smoke in this read-only review.

Real behavior proof
Needs real behavior proof before merge: The PR has tests and a textual claim of redacted local exercise, but no after-fix terminal output, logs, screenshot, recording, or linked artifact showing real container behavior. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, ask a maintainer to comment @clawsweeper re-review.

Next step before merge
This is an active external implementation PR for a new transport/config surface; the blocker is contributor-supplied real behavior proof plus maintainer review, not an autonomous replacement fix.

Security
Cleared: The current diff adds ws and user-configured REST/WebSocket calls, but this pass found no concrete secrets, permission, or supply-chain regression after raw payload logging was removed.

Review details

Best possible solution:

Ship the plugin-local Signal transport adapter after maintainer review, with native behavior preserved, config/docs aligned, targeted tests green, and redacted live bbernhard container proof attached to the PR.

Do we have a high-confidence way to reproduce the issue?

Yes, source inspection gives a high-confidence reproduction path: current main always calls native /api/v1/rpc, /api/v1/check, and /api/v1/events, while bbernhard container docs expose /v2/send and /v1/receive/{number}. I did not run a live container smoke in this read-only review.

Is this the best way to solve the issue?

Yes in direction: a Signal-plugin-local adapter is the right boundary for supporting native and container transports. It is not merge-ready until the PR has redacted real behavior proof and maintainer review of the new transport/dependency surface.

Acceptance criteria:

  • pnpm test extensions/signal/src/client.test.ts extensions/signal/src/client-adapter.test.ts extensions/signal/src/client-container.test.ts extensions/signal/src/monitor.tool-result.pairs-uuid-only-senders-uuid-allowlist-entry.test.ts
  • pnpm test:extension signal
  • pnpm config:docs:check
  • pnpm check:deprecated-internal-config-api
  • Live bbernhard/signal-cli-rest-api container smoke for send, receive, attachment, group, reaction, and receipt paths with redacted proof

What I checked:

Likely related people:

  • steipete: GitHub commit history shows repeated recent work in Signal runtime/config paths, and this PR branch includes current maintainer commits on the same transport surface. (role: recent Signal runtime contributor and reviewer; confidence: high; commits: 3b0ed18b8679, 537a8e25ed5d, 95331e5cc52d; files: extensions/signal/src/client.ts, extensions/signal/src/monitor.ts, extensions/signal/src/send.ts)
  • vincentkoc: Recent history shows multiple edits to Signal/channel documentation and adjacent configuration documentation, which this PR updates. (role: Signal docs and config-adjacent contributor; confidence: medium; commits: f4129cdd2ba8, 1042b893f617, f051204bea1e; files: docs/channels/signal.md, docs/gateway/config-channels.md)
  • scoootscooob: The central Signal implementation was moved into extensions/signal/src by this contributor, which is relevant to the plugin-boundary review for the new transport files. (role: Signal extension migration contributor; confidence: medium; commits: 4540c6b3bc12; files: extensions/signal/src/client.ts, extensions/signal/src/monitor.ts, extensions/signal/src/send.ts)

Remaining risk / open question:

  • No after-fix real behavior artifact demonstrates bbernhard container send, receive, attachment, group, reaction, and receipt paths.
  • The PR adds a new runtime WebSocket dependency and transport mode, so maintainer review of dependency ownership and runtime behavior is still warranted.

Codex review notes: model gpt-5.5, reasoning high; reviewed against 9cf0651e46f7.

@Hua688

Hua688 commented May 1, 2026

Copy link
Copy Markdown
Contributor Author

Updated this branch to address the current ClawSweeper/maintainer findings while keeping the original adapter boundary intact.

What changed:

  • Removed raw Signal WebSocket frame logging from the container receive path. The WebSocket still logs connect/close/error/parse failures, but no longer logs inbound message payload snippets.
  • Forwarded timeoutMs through the adapter stream path so native SSE keeps the existing idle-timeout behavior and container streams can receive the same option.
  • Made signalCheck() retry-friendly in auto mode: if auto-detection fails while the service is still starting, it now returns { ok: false, status: null, error } instead of throwing and aborting waitForTransportReady() polling.
  • Replaced the broad openclaw/plugin-sdk/config-runtime import with openclaw/plugin-sdk/runtime-config-snapshot in the adapter, and replaced the container fetch import with the narrow openclaw/plugin-sdk/fetch-runtime seam.
  • Added strict schema support for top-level channels.signal.apiMode (auto | native | container) and kept per-account apiMode rejected.
  • Updated focused tests and regenerated the config baseline hash.

Why the adapter still reads config:

The adapter is intentionally the only layer that knows whether Signal is using native JSON-RPC/SSE or the bbernhard REST/WebSocket container. Callers such as send/monitor/probe should continue to express Signal operations, not branch on apiMode or pass mode-specific clients around. To preserve that boundary, the adapter reads the resolved runtime config snapshot internally and selects the native or container transport there. The transport clients themselves (client.ts and client-container.ts) remain config-free and only know their respective protocol endpoints.

This keeps apiMode from leaking through the Signal call sites while avoiding the previous broad config-runtime import.

Validation run locally:

  • pnpm test extensions/signal
  • pnpm tsgo:extensions && pnpm tsgo:test:extensions
  • pnpm check:deprecated-internal-config-api
  • pnpm config:docs:check
  • targeted oxfmt --check

Live container note: this branch has been exercised against a local bbernhard/signal-cli-rest-api Docker deployment for real Signal traffic, with identifiers and message contents redacted. I am not adding unredacted account/message details to the PR.

@Hua688

Hua688 commented May 1, 2026

Copy link
Copy Markdown
Contributor Author

Updated the branch for the latest ClawSweeper findings and rebased it onto current upstream/main.

Fixes included:

  • containerRestRequest() now only treats 204 No Content as bodyless, so bbernhard /v2/send 201 Created responses are parsed and the returned timestamp is preserved as the message id instead of falling back to "unknown".
  • streamContainerEvents() now logs a redacted receive endpoint (/v1/receive/<redacted>) instead of the full WebSocket URL containing the Signal account identifier.
  • Added focused coverage for both behaviors in extensions/signal/src/client-container.test.ts.

Validation run locally:

  • pnpm test extensions/signal
  • pnpm test extensions/signal/src/client-container.test.ts
  • pnpm config:docs:check
  • pnpm check:deprecated-internal-config-api
  • git diff --check

Latest pushed head: 53951ad3bcceae2c4deee1cea1fc5d95ae8c0e8e.

@unn-Known1

Copy link
Copy Markdown

🤖 PR Review Summary

Status: ⚠️ Merge Conflicts Detected

Analysis

  • What the PR does: Adds REST API support for containerized Signal deployments via bbernhard/signal-cli-rest-api Docker image. Includes auto-detection, new container client adapter, WebSocket event streaming, and base64 attachment encoding.
  • Impact Assessment: 🟢 Beneficial - Fixes a significant gap in Signal channel support. Well-documented with 186 passing tests.
  • CI Status: Unknown (mergeable_state: unknown)

Conflict Details

  • Status: Branch has diverged from main (911 commits behind)
  • Resolution Needed: Author needs to rebase onto latest main

Next Steps

  1. Rebase onto latest main:
    git fetch origin
    git rebase origin/main
  2. Resolve any conflicts (especially in Signal-related files)
  3. Force-push after rebase

Automated review by GitHub Manager Agent

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: signal Channel integration: signal docs Improvements or additions to documentation proof: supplied External PR includes structured after-fix real behavior proof. size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Signal: Add REST API support alongside JSON-RPC for containerized deployments

3 participants