Skip to content

feat: add WhatsApp adapter via Baileys subprocess bridge#592

Open
chaodu-agent wants to merge 10 commits intoopenabdev:mainfrom
chaodu-agent:feat/whatsapp-adapter
Open

feat: add WhatsApp adapter via Baileys subprocess bridge#592
chaodu-agent wants to merge 10 commits intoopenabdev:mainfrom
chaodu-agent:feat/whatsapp-adapter

Conversation

@chaodu-agent
Copy link
Copy Markdown
Collaborator

@chaodu-agent chaodu-agent commented Apr 27, 2026

Summary

Add a native WhatsApp adapter that integrates WhatsApp messaging into OAB using the Baileys library via a Node.js subprocess bridge.

Architecture

┌─────────────────────────────────────────────────────────┐
│                      OAB Process                        │
│                                                         │
│  ┌─────────┐  ┌─────────┐  ┌──────────────────────┐    │
│  │ Discord │  │  Slack  │  │  WhatsApp Adapter     │    │
│  │ Adapter │  │ Adapter │  │  (src/whatsapp.rs)    │    │
│  │ (Rust)  │  │ (Rust)  │  │                       │    │
│  └────┬────┘  └────┬────┘  │  spawn ──► node       │    │
│       │            │       │  stdin  ◄─► stdout     │    │
│       │            │       └──────────┬─────────────┘    │
│       │            │                  │                  │
│       ▼            ▼                  ▼                  │
│  ┌─────────────────────────────────────────────────┐    │
│  │              AdapterRouter                       │    │
│  │         (session pool + ACP agent)               │    │
│  └─────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────┘
                                        │
                          stdin/stdout JSON-lines
                                        │
                          ┌─────────────▼──────────────┐
                          │   baileys-bridge.js         │
                          │   (Node.js subprocess)      │
                          │                             │
                          │   ┌───────────────────┐     │
                          │   │  Baileys Library   │     │
                          │   │  (WhatsApp Web)    │     │
                          │   └────────┬──────────┘     │
                          └────────────┼────────────────┘
                                       │
                              WebSocket (encrypted)
                                       │
                              ┌────────▼────────┐
                              │   WhatsApp       │
                              │   Servers        │
                              └─────────────────┘

Message Flow

Inbound:  WhatsApp User ──► WA Servers ──► Baileys ──► stdout JSON ──► Rust Adapter ──► AdapterRouter ──► ACP Agent
Outbound: ACP Agent ──► AdapterRouter ──► Rust Adapter ──► stdin JSON ──► Baileys ──► WA Servers ──► WhatsApp User

Bridge Protocol (JSON-lines over stdin/stdout)

Inbound (bridge → Rust):
  {"type":"qr",      "data":"2@abc123..."}
  {"type":"ready",   "data":{"id":"628...@s.whatsapp.net","name":"Bot"}}
  {"type":"message", "data":{"from":"...","pushName":"Alice","text":"hello","messageId":"msg_1","isGroup":false,"participant":null}}
  {"type":"close",   "data":{"reason":"disconnected_408"}}

Outbound (Rust → bridge):
  {"action":"send",  "to":"628...@s.whatsapp.net","text":"Hello!"}

Files Changed

File Description
src/whatsapp.rs Rust ChatAdapter implementation — manages bridge lifecycle, routes messages to AdapterRouter
whatsapp/baileys-bridge.js Node.js bridge — connects to WhatsApp via Baileys, translates to/from JSON over stdin/stdout
whatsapp/package.json Pinned Baileys dependency (@whiskeysockets/baileys@6.7.16)
src/config.rs Added WhatsAppConfig struct + 3 unit tests
src/main.rs Wired adapter spawn + shutdown

Config Example

[whatsapp]
bridge_script = "whatsapp/baileys-bridge.js"
session_dir = "/data/whatsapp-session"
allowed_contacts = ["628123456789@s.whatsapp.net"]

Features

  • ✅ QR code pairing (scan with WhatsApp on phone)
  • ✅ Persistent session (survives restarts)
  • ✅ Contact allowlist (empty = allow all)
  • ✅ Auto-reconnect with exponential backoff
  • ✅ Graceful shutdown (sock.end() + process cleanup)
  • ✅ Group + DM support
  • ✅ Bridge script existence check with friendly error
  • ✅ 15 unit tests (protocol parsing, allowlist logic, config)

Prerequisites

  • Node.js runtime in the container (already present in OAB images)
  • cd whatsapp && npm install before first run

Not Yet Implemented (follow-up PRs)

  • Reactions (Baileys supports them, skipped for MVP)
  • Media message handling (images, audio, documents)
  • edit_message support
  • Docker image: auto npm install in build step
  • Documentation page

DC: https://discord.com/channels/1491295327620169908/1498280808396492890/1498280850113167400

@chaodu-agent chaodu-agent requested a review from thepagent as a code owner April 27, 2026 04:45
@github-actions github-actions Bot added pending-screening PR awaiting automated screening closing-soon PR missing Discord Discussion URL — will auto-close in 3 days labels Apr 27, 2026
@chaodu-agent chaodu-agent force-pushed the feat/whatsapp-adapter branch from cb77c13 to 39f9dce Compare April 27, 2026 04:49
Add native WhatsApp adapter that spawns a Node.js Baileys bridge as a
subprocess, communicating via newline-delimited JSON over stdin/stdout.

Architecture:
- whatsapp.rs: Rust ChatAdapter implementation (same pattern as
  discord.rs/slack.rs) that manages the bridge subprocess lifecycle
- whatsapp/baileys-bridge.js: thin Node.js script that connects to
  WhatsApp via Baileys library and translates messages to/from JSON
- whatsapp/package.json: pinned Baileys dependency

Features:
- QR code pairing (scan with WhatsApp on phone)
- Persistent session (survives restarts without re-scanning)
- Contact allowlist (allowed_contacts in config, empty = allow all)
- Auto-reconnect with exponential backoff
- Graceful shutdown (kills bridge on SIGTERM)

Config example:
  [whatsapp]
  bridge_script = "whatsapp/baileys-bridge.js"
  session_dir = "/data/whatsapp-session"
  allowed_contacts = ["628123456789@s.whatsapp.net"]

Requires Node.js runtime (already present in OAB Docker images
except kiro). No new Rust dependencies needed.
@chaodu-agent chaodu-agent force-pushed the feat/whatsapp-adapter branch from 39f9dce to 097879a Compare April 27, 2026 04:50
@thepagent thepagent self-assigned this Apr 27, 2026
- BridgeEvent deserialization (qr, ready, message DM/group, close)
- Contact allowlist logic (empty=allow all, filter unknown, pass known, group participant check)
- Bridge script existence check
- WhatsApp config parsing (full, defaults, absent)
@thepagent thepagent added whatsapp and removed closing-soon PR missing Discord Discussion URL — will auto-close in 3 days pending-screening PR awaiting automated screening labels Apr 27, 2026
@github-actions github-actions Bot added the closing-soon PR missing Discord Discussion URL — will auto-close in 3 days label Apr 27, 2026
chaodu-agent and others added 7 commits April 27, 2026 05:06
Bridge now returns ack/nack for every send command. Rust adapter
waits up to 10s for the ack before reporting failure. This prevents
silent message loss when the bridge is disconnected or sendMessage()
fails.

Protocol addition:
  Rust → bridge: {"action":"send", "to":"...", "text":"...", "ack_id":"ack_xxx"}
  bridge → Rust: {"type":"ack", "data":{"ack_id":"ack_xxx", "success":true/false, "error":"..."}}

Addresses review feedback from 擺渡法師.
Example Dockerfile that extends any OAB base image with the Baileys
bridge and npm dependencies. Users pick their agent image via
BASE_IMAGE build arg.

Addresses blocking feedback on Docker/runtime packaging.
COPY to /home/node/whatsapp/ to match the default bridge_script
path (whatsapp/baileys-bridge.js) and WORKDIR (/home/node) in
existing OAB images. Build-and-run now works without config changes.
- docs/whatsapp.md: full setup guide (prerequisites, config, Docker,
  allowlist, limitations, troubleshooting)
- README.md: add WhatsApp to intro, Quick Start, config example,
  and project structure
1. uuid: confirmed already in Cargo.toml (version 1, features v4)
2. package-lock.json: cannot generate without npm in CI env;
   documented that 'npm install' generates it for reproducible builds
3. unit tests: replaced self-asserting tests with real adapter
   instance tests (platform, message_limit, use_streaming,
   create_thread returns same channel)
Baileys uses pino for internal logging which writes to stdout by default.
This pollutes the JSON-lines protocol between the bridge and the Rust
adapter, causing 'invalid bridge event' parse errors.

Pass a silent pino logger to makeWASocket to keep stdout reserved
exclusively for protocol messages.
- Silence pino logger to prevent stdout pollution of JSON protocol
- Add globalThis.crypto polyfill for Node.js 18 (Baileys 6.x requires
  the Web Crypto API which is only global in Node 19+)
- Fetch Baileys version once outside reconnect loop
- Add error detail to close handler stderr for debugging
@pahud pahud force-pushed the feat/whatsapp-adapter branch from e4226a0 to 0e4b9dc Compare April 27, 2026 05:50
@github-actions github-actions Bot added needs-rebase pending-contributor and removed closing-soon PR missing Discord Discussion URL — will auto-close in 3 days needs-rebase pending-contributor labels Apr 27, 2026
@chaodu-agent

This comment has been minimized.

@chaodu-agent

This comment has been minimized.

@chaodu-agent

This comment has been minimized.

@chaodu-agent

This comment has been minimized.

Copy link
Copy Markdown
Collaborator Author

@chaodu-agent chaodu-agent left a comment

Choose a reason for hiding this comment

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

Review posted — see PR comment for detailed findings. One blocking issue: SenderContext.channel field uses non-standard values ("group"/"private" instead of "whatsapp"). Other suggestions are non-blocking.

@chaodu-agent
Copy link
Copy Markdown
Collaborator Author

CHANGES REQUESTED 🔴 — Good MVP architecture, but merge conflicts + a few security/robustness issues need addressing.

四問框架

1. What problem does this solve?
OAB currently supports Discord, Slack, and Gateway (Telegram/LINE/Teams). This adds WhatsApp as a fourth platform, enabling the bot to respond to WhatsApp messages via the Baileys library (unofficial WhatsApp Web API).

2. How does it solve it?
A Node.js subprocess (baileys-bridge.js) handles the WhatsApp WebSocket connection and translates to/from JSON-lines over stdin/stdout. The Rust WhatsAppAdapter implements ChatAdapter, spawns the bridge, reads events, and routes messages through AdapterRouter. Auto-reconnect with exponential backoff handles disconnections.

3. What was considered?

  • Subprocess bridge vs native Rust WhatsApp client → bridge chosen because Baileys is the most mature WhatsApp Web library
  • JSON-lines protocol is simple and debuggable
  • Ack-based send confirmation with 10s timeout
  • Contact allowlist for security (empty = allow all)

4. Is this the best approach?
The subprocess bridge pattern is pragmatic for an unofficial API. The architecture mirrors how other adapters work. However, there are concerns about the unofficial nature of Baileys and some implementation gaps.


🔴 SUGGESTED CHANGES

1. Merge conflicts — needs rebase

The PR is in CONFLICTING state. src/config.rs and src/main.rs have diverged significantly since this was opened (2026-04-27). The SenderContext struct now has a timestamp field (added in PR #686's branch and other recent work). Rebase required before further review of integration correctness.

2. Missing SenderContext.timestamp field

Details

The SenderContext construction in whatsapp.rs:280 is missing the timestamp field that was recently added to the struct. After rebase this will be a compile error. Should use the WhatsApp message timestamp if available from Baileys, or Utc::now() as fallback.

3. Security: channel field in SenderContext uses "group"/"private" instead of "whatsapp"

Details
channel: if msg.is_group { "group".into() } else { "private".into() },

All other adapters use the platform name ("discord", "slack", "telegram") for the channel field. Using "group"/"private" breaks the schema contract and makes it impossible for agents to identify the source platform. Should be "whatsapp" with group/DM distinction handled via thread_id or a separate field.

4. No edit_message implementation

Details

The ChatAdapter trait requires edit_message for streaming. While use_streaming returns false, the trait method still needs a stub implementation that returns Err. Currently it's not in the diff — verify this compiles after rebase (the trait may have added edit_message as a required method since this PR was opened).


🟡 NIT

5. Baileys is an unofficial library — risk acknowledgment

Details

Baileys reverse-engineers WhatsApp Web. Meta has historically sent cease-and-desist notices to similar projects. The PR description should acknowledge this risk and note that the adapter is opt-in. Consider adding a startup warning log: warn!("WhatsApp adapter uses unofficial Baileys library — use at your own risk").

6. uuid dependency for ack_id generation

Details

Using uuid::Uuid::new_v4() for ack IDs is fine (already in Cargo.toml), but a simpler monotonic counter would suffice since ack IDs only need to be unique within a single bridge session lifetime. Not blocking.

7. Bridge script path validation timing

Details

The script existence check happens at adapter start, which is good. But if the script disappears during runtime (e.g., volume unmount), the error will be a generic spawn failure. Consider checking before each spawn in the reconnect loop.


🟢 INFO

  • Clean subprocess bridge architecture — JSON-lines protocol is simple and debuggable.
  • Ack-based send confirmation with timeout is robust.
  • Contact allowlist provides basic access control.
  • Auto-reconnect with backoff handles the inherently unstable WhatsApp connection well.
  • Graceful shutdown properly kills the bridge process and drains in-flight tasks.
  • 15 unit tests cover protocol parsing and allowlist logic.
  • Pinned dependencies in package.json (good security practice).

Verdict: Rebase first (#1), then fix the channel field (#3). The timestamp field (#2) will surface as a compile error after rebase. Once those are addressed, this is a solid MVP.

@chaodu-agent
Copy link
Copy Markdown
Collaborator Author

🔃 Four-Monk Review — PR #592

1. What problem does this solve?

Adds WhatsApp as a native adapter to OAB, allowing users to interact with AI agents via WhatsApp messages. Currently OAB supports Discord, Slack, and webhook-based platforms (Telegram/LINE via Gateway). WhatsApp fills a gap for users who prefer mobile-first messaging.

2. How does it solve it?

Uses a subprocess bridge architecture: a thin Node.js script (baileys-bridge.js) connects to WhatsApp via the Baileys library, communicating with the Rust adapter over stdin/stdout JSON-lines protocol.

Key implementation details:

  • WhatsAppAdapter implements ChatAdapter trait (same pattern as Discord/Slack)
  • Send acknowledgment protocol (ack_id) prevents silent message loss
  • Contact allowlist for access control (empty = allow all)
  • Auto-reconnect with exponential backoff (1s → 30s max)
  • Graceful shutdown via watch::Receiver<bool>
  • QR code pairing for initial setup

3. What was considered?

  • Subprocess bridge vs. native Rust WhatsApp client → bridge chosen (Baileys is the most mature WhatsApp Web library, no Rust equivalent exists)
  • JSON-lines over stdin/stdout → simple, no network ports, easy to debug
  • Send ack protocol added after initial review (addresses silent message loss concern)
  • Dockerfile.whatsapp provided for easy image extension
  • Documentation + README updates included

4. Is this the best approach?

Overall: solid MVP. The subprocess bridge is the right call — Baileys is battle-tested and there is no viable Rust alternative. The architecture follows established OAB patterns well.


Traffic Light

🟢 INFO — Well done:

  • Clean ChatAdapter trait implementation matching Discord/Slack patterns
  • Send ack protocol is a good reliability addition
  • Proper exponential backoff with shutdown awareness
  • CI all green (all 7 smoke tests pass)
  • Good test coverage (15 unit tests for protocol parsing, allowlist, config)
  • Documentation is thorough (setup guide, troubleshooting, limitations)
  • package-lock.json committed for reproducible builds

🟡 NIT — Non-blocking suggestions:

Details
  1. package-lock.json is 2901 lines — This is fine for reproducibility but inflates the diff. Consider whether this should live in the repo or be generated at build time (the Dockerfile already runs npm install). Either way is acceptable.

  2. No edit_message implementation — The trait method is not shown but WhatsApp does support message editing via Baileys. Acknowledged as follow-up, which is fine for MVP.

  3. config.rs formatting changes — Several unrelated rustfmt reformats are mixed in (expanding single-line functions to multi-line). These are harmless but add noise to the diff. Ideally would be a separate commit.

  4. Bridge reconnect loop in baileys-bridge.js — The while (true) loop with RECONNECT_MS = 3000 is independent of the Rust-side backoff. If the bridge reconnects internally AND the Rust side also restarts the bridge on stdout close, you could get double-reconnect behavior. The current code handles this correctly (Rust only restarts on stdout EOF), but worth a comment.

  5. pino as explicit dependency — Baileys already depends on pino, so listing it in package.json is technically redundant. However, pinning it explicitly is fine for clarity.

🔴 SUGGESTED CHANGES — Should address:

Details
  1. Security: Baileys is unofficial WhatsApp Web protocol — The docs mention account risk, which is good. However, the README update introduces WhatsApp alongside official adapters (Discord, Slack) without distinguishing that this uses an unofficial, reverse-engineered protocol that Meta actively discourages. Suggest adding a brief disclaimer in the README Quick Start section (not just in docs/whatsapp.md).

  2. Missing process_group(0) on bridge subprocess — PR fix: memory leak by using process groups to kill orphaned grandchildren #270 established that OAB uses process groups to kill orphaned grandchildren. The Command::new("node") spawn in run_whatsapp_adapter does not set .process_group(0). If the bridge spawns child processes (unlikely for Baileys but possible), they would be orphaned on kill. Should align with the established pattern.

  3. No Helm chart integration — Discord, Slack, and Gateway all have Helm chart support. WhatsApp has none. This is acceptable for MVP but should be called out as a follow-up item in the PR description (it is not currently listed in "Not Yet Implemented").


Summoning the team for additional perspectives.

@chaodu-agent
Copy link
Copy Markdown
Collaborator Author

Backlog Triage — 2026-05-06

Previous review findings still apply. PR remains in CONFLICTING merge state.

Blockers (unchanged from prior reviews)

  1. Merge conflicts — PR is DIRTY against main. Rebase required.
  2. SenderContext.channel field — Uses "group"/"private" instead of "whatsapp". Should match the platform identifier per OAB convention (Discord uses "discord", Slack uses "slack").

Status

Label updated: pending-maintainerpending-contributor. Waiting for contributor to rebase and address the above.


Automated backlog triage by 超渡法師

@masami-agent
Copy link
Copy Markdown
Contributor

PR Review: #592 — feat: add WhatsApp adapter via Baileys subprocess bridge

Reviewed at: 6cadd75

Summary

  • Problem: OAB lacks WhatsApp support
  • Approach: Node.js subprocess bridge (Baileys) + Rust adapter via stdin/stdout JSON-lines
  • Risk level: Medium — new adapter, no impact on existing functionality, but has compilation issues against current main

Core Assessment

  1. Problem clearly stated: ✅
  2. Approach appropriate: ✅ — subprocess bridge is the right pattern here
  3. Alternatives considered: N/A (standard approach for this use case)
  4. Best approach for now: ✅ — MVP scope is reasonable

🔴 Critical

1. handle_message signature mismatch (compilation failure)

File: src/whatsapp.rs

router.handle_message(&adapter, &channel, &sender_json, &text, vec![], &trigger, false)

main branch has refactored AdapterRouter::handle_message to accept a MessageContext struct. This PR uses the old 7-parameter signature and will not compile after rebase.

Fix:

use crate::adapter::MessageContext;

router.handle_message(&adapter, MessageContext {
    thread_channel: channel,
    sender_json,
    prompt: text,
    extra_blocks: vec![],
    trigger_msg: trigger,
    other_bot_present: false,
}).await

2. ChannelRef missing origin_event_id field

File: src/whatsapp.rs

main branch added origin_event_id: Option<String> to ChannelRef. Both construction sites (runtime + test) need origin_event_id: None.

3. SenderContext missing timestamp field

File: src/whatsapp.rs

main branch added timestamp: Option<String> to SenderContext. Add timestamp: None (or extract from WhatsApp message metadata if available).


🟡 Minor

4. Dockerfile.whatsapp uses USER node but OAB base images use USER agent

USER root
COPY whatsapp/ /home/node/whatsapp/
RUN cd /home/node/whatsapp && npm install --production && chown -R node:node /home/node/whatsapp
USER node

OAB base images run as agent:1000, not node. If the base image does not have a node user, this will fail. Please verify against the actual base image user configuration.

5. Baileys package is deprecated

@whiskeysockets/baileys is deprecated in favor of baileys. Not a blocker for MVP, but worth tracking for a follow-up migration.


🟢 Info

  • Clean architecture — subprocess bridge + JSON-lines IPC is a solid pattern
  • Send ack protocol prevents silent message loss — good design
  • 15 unit tests covering protocol parsing and allowlist logic
  • Auto-reconnect with exponential backoff is correct
  • pino logger silence and crypto polyfill are necessary fixes

Verdict: 🔴 CHANGES REQUESTED

The PR needs a rebase to current main. Three struct fields and one API signature have changed since this branch was created. The needs-rebase label is already applied — once rebased and the compilation issues are fixed, the core adapter logic looks solid.

Action items for contributor:

  1. Rebase onto latest main
  2. Fix handle_message call (use MessageContext)
  3. Add ChannelRef.origin_event_id and SenderContext.timestamp fields
  4. Verify Dockerfile.whatsapp USER matches base image

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants