Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
1aad670
feat(messaging): add WeChat (personal) channel
sandl99 May 7, 2026
862506f
feat(messaging): register WeChat channel post-install and cover with …
sandl99 May 8, 2026
29b0823
refactor(messaging): restructure WeChat integration per review
sandl99 May 10, 2026
5ef7e8f
refactor(messaging): move WeChat plugin to base, scope Hermes follow-up
sandl99 May 11, 2026
9d14bfb
feat(messaging): guard host-qr handler errors + document WeChat channel
sandl99 May 11, 2026
3b68383
fix(messaging): true try/catch on host-qr handler + doc style cleanup
sandl99 May 11, 2026
921d5b6
docs(messaging): active voice + drop redundant em dashes per style guide
sandl99 May 11, 2026
ab29142
feat(messaging): channels add wechat + base-image symlink whitelist
sandl99 May 11, 2026
69ec2d1
fix(messaging): address CodeRabbit findings on wechat-integration PR
sandl99 May 11, 2026
d5edf8b
fix(messaging): move WeChat plugin install back to production Dockerfile
sandl99 May 11, 2026
9696525
fix(messaging): stop/add/policy gaps that broke channels.add wechat a…
sandl99 May 12, 2026
ee80d64
fix(messaging): address post-rebase CodeQL + CodeRabbit findings
sandl99 May 12, 2026
19e8dbe
test(cli,onboard): raise budgets on three subprocess-spawning tests
sandl99 May 12, 2026
979af1d
fix(messaging): address remaining CodeRabbit findings on wechat-integ…
sandl99 May 12, 2026
b578f2b
fix(messaging): normalize host-side symlink audit path for Windows
sandl99 May 12, 2026
a100d29
refactor(messaging): narrow wechat PR to leverage #3395, add wechat e2e
sandl99 May 13, 2026
edd630a
refactor(onboard): extract host-qr/wechat/channel-setup to onboard/ h…
sandl99 May 13, 2026
e48eac0
fix(messaging): address CodeRabbit on host-qr scanner merge + token l…
sandl99 May 13, 2026
f177e53
test(onboard): pass wechatConfig in dockerfile patch base-URL overrid…
sandl99 May 14, 2026
ac10c66
chore(deps): sync package-lock with main after rebase
sandl99 May 14, 2026
f00e724
chore(e2e): regenerate parity inventory after rebase
sandl99 May 14, 2026
7b4339c
chore(e2e): classify new WeChat M-W* legacy assertions in parity map
sandl99 May 14, 2026
4dd94f1
Merge remote-tracking branch 'origin/main' into u/sdang/wechat-integr…
cv May 15, 2026
f1a66c8
Merge remote-tracking branch 'origin/main' into u/sdang/wechat-integr…
sandl99 May 15, 2026
c28f3c6
merge: origin/main into wechat integration
sandl99 May 15, 2026
44d0a25
fix(messaging): include wechat in reuse and policy paths
sandl99 May 15, 2026
e4286e1
Merge branch 'main' into u/sdang/wechat-integration
cv May 15, 2026
13cebe6
merge: origin/main into wechat integration
cv May 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 35 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -225,10 +225,12 @@ COPY scripts/nemoclaw-start.sh /usr/local/bin/nemoclaw-start
COPY nemoclaw-blueprint/scripts/*.js /usr/local/lib/nemoclaw/preloads/
COPY scripts/codex-acp-wrapper.sh /usr/local/bin/nemoclaw-codex-acp
COPY scripts/generate-openclaw-config.py /usr/local/lib/nemoclaw/generate-openclaw-config.py
COPY scripts/seed-wechat-accounts.py /usr/local/lib/nemoclaw/seed-wechat-accounts.py
COPY nemoclaw-blueprint/openclaw-plugins/ /usr/local/share/nemoclaw/openclaw-plugins/
RUN chmod 755 /usr/local/bin/nemoclaw-start /usr/local/bin/nemoclaw-codex-acp \
/usr/local/lib/nemoclaw/sandbox-init.sh \
/usr/local/lib/nemoclaw/generate-openclaw-config.py \
/usr/local/lib/nemoclaw/seed-wechat-accounts.py \
&& if [ -d /usr/local/lib/nemoclaw/preloads ]; then find /usr/local/lib/nemoclaw/preloads -type f -name '*.js' -exec chmod 644 {} +; fi \
&& chmod 755 /usr/local/share/nemoclaw \
/usr/local/share/nemoclaw/openclaw-plugins \
Expand Down Expand Up @@ -279,6 +281,12 @@ ARG NEMOCLAW_DISCORD_GUILDS_B64=e30=
# When requireMention is true, Telegram groups get groups: {"*": {"requireMention": true}}
# with groupPolicy: open. See #1737, #3022. Default: empty map.
ARG NEMOCLAW_TELEGRAM_CONFIG_B64=e30=
# Base64-encoded JSON WeChat config (e.g.
# {"accountId":"…","baseUrl":"https://…","userId":"…"}).
# Captured by the host-side iLink QR login during onboard. Non-secret per-account
# metadata only — the bot token flows through the OpenShell provider, never
# baked into the image. Default: empty map.
ARG NEMOCLAW_WECHAT_CONFIG_B64=e30=
# Set to "1" to force-disable device-pairing auth. Also auto-disabled when
# CHAT_UI_URL is a non-loopback address (Brev Launchable, remote deployments)
# since terminal-based pairing is impossible in those contexts.
Expand Down Expand Up @@ -325,6 +333,7 @@ ENV NEMOCLAW_MODEL=${NEMOCLAW_MODEL} \
NEMOCLAW_MESSAGING_ALLOWED_IDS_B64=${NEMOCLAW_MESSAGING_ALLOWED_IDS_B64} \
NEMOCLAW_DISCORD_GUILDS_B64=${NEMOCLAW_DISCORD_GUILDS_B64} \
NEMOCLAW_TELEGRAM_CONFIG_B64=${NEMOCLAW_TELEGRAM_CONFIG_B64} \
NEMOCLAW_WECHAT_CONFIG_B64=${NEMOCLAW_WECHAT_CONFIG_B64} \
NEMOCLAW_DISABLE_DEVICE_AUTH=${NEMOCLAW_DISABLE_DEVICE_AUTH} \
NEMOCLAW_PROXY_HOST=${NEMOCLAW_PROXY_HOST} \
NEMOCLAW_PROXY_PORT=${NEMOCLAW_PROXY_PORT} \
Expand Down Expand Up @@ -360,11 +369,32 @@ USER sandbox
# list of env vars and derivation rules.
RUN python3 /usr/local/lib/nemoclaw/generate-openclaw-config.py

# Install NemoClaw plugin into OpenClaw. Prune non-runtime metadata from
# staged bundled plugin dependencies before this layer is committed; deleting
# it in a later layer would not reduce the OCI image imported by k3s.
# TEMPORARY: install the WeChat plugin here (was moved to Dockerfile.base in
# e23486b but the wholesale rewrite by generate-openclaw-config.py above
# blew away plugins.installs.openclaw-weixin from base's openclaw.json,
# leaving the plugin unloadable at runtime and taking Telegram down with it).
# Running the install AFTER generate-openclaw-config.py merges the registry
# entry into the freshly-written config. Seed the per-account state right
# after so the bridge picks up the captured iLink session.
# hadolint ignore=DL3059,DL4006
RUN (openclaw doctor --fix > /dev/null 2>&1 || true) \
&& (openclaw plugins install /opt/nemoclaw > /dev/null 2>&1 || true) \
&& openclaw plugins install \
'@tencent-weixin/openclaw-weixin@2.4.2' --pin \
&& openclaw config set plugins.entries.openclaw-weixin.enabled true \
&& python3 /usr/local/lib/nemoclaw/seed-wechat-accounts.py

# Lock down npm: no further registry traffic in this image. Everything past
# this point must resolve from local sources only.
ENV NPM_CONFIG_OFFLINE=true \
NPM_CONFIG_AUDIT=false \
NPM_CONFIG_FUND=false

# Install NemoClaw plugin into OpenClaw (local /opt/nemoclaw, no network).
# Prune non-runtime metadata from staged bundled plugin dependencies before
# this layer is committed; deleting it in a later layer would not reduce the
# OCI image imported by k3s.
# hadolint ignore=DL3059,DL4006
RUN (openclaw plugins install /opt/nemoclaw > /dev/null 2>&1 || true) \
&& if [ -d /sandbox/.openclaw/plugin-runtime-deps ]; then \
find /sandbox/.openclaw/plugin-runtime-deps -type f \( \
-name '*.d.ts' -o -name '*.d.mts' -o -name '*.d.cts' -o \
Expand Down Expand Up @@ -474,6 +504,7 @@ RUN set -eu; \
"$config_dir/flows" \
"$config_dir/sandbox" \
"$config_dir/telegram" \
"$config_dir/wechat" \
"$config_dir/media" \
"$config_dir/plugin-runtime-deps"; \
touch "$config_dir/update-check.json" "$config_dir/exec-approvals.json"; \
Expand Down
1 change: 0 additions & 1 deletion Dockerfile.base
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@
# Dockerfile and Dockerfile.base
# 5. New .openclaw subdirectory — add mkdir below
# 6. PyYAML or other pip dep bump — change the version below
#
# For ad-hoc rebuilds (e.g., security patch), use workflow_dispatch on
# the base-image workflow.
#
Expand Down
2 changes: 2 additions & 0 deletions agents/openclaw/manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ state_dirs:
- cron
- memory
- telegram
- wechat
- credentials

# ── Authentication ──────────────────────────────────────────────
Expand All @@ -63,6 +64,7 @@ messaging_platforms:
- telegram
- discord
- slack
- wechat

# ── Inference ───────────────────────────────────────────────────
inference:
Expand Down
79 changes: 67 additions & 12 deletions docs/manage-sandboxes/messaging-channels.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ title:
page: "Set Up Messaging Channels with NemoClaw and OpenShell"
nav: "Set Up Messaging Channels"
description:
main: "Connect Telegram, Discord, or Slack to your sandboxed OpenClaw agent using OpenShell-managed channel messaging."
agent: "Explains how Telegram, Discord, and Slack reach the sandboxed OpenClaw agent through OpenShell-managed processes and NemoClaw channel commands. Use when setting up messaging channels, chat interfaces, or integrations without relying on nemoclaw tunnel start for bridges."
keywords: ["nemoclaw messaging channels", "nemoclaw telegram", "nemoclaw discord", "nemoclaw slack", "openshell channel messaging"]
main: "Connect Telegram, Discord, Slack, or WeChat to your sandboxed OpenClaw agent using OpenShell-managed channel messaging."
agent: >-
Explains how Telegram, Discord, Slack, and WeChat reach the sandboxed OpenClaw agent through OpenShell-managed processes and NemoClaw channel commands.
Use when setting up messaging channels, chat interfaces, or integrations without relying on `nemoclaw tunnel start` for bridges.
keywords: ["nemoclaw messaging channels", "nemoclaw telegram", "nemoclaw discord", "nemoclaw slack", "nemoclaw wechat", "openshell channel messaging"]
topics: ["generative_ai", "ai_agents"]
tags: ["openclaw", "openshell", "telegram", "discord", "slack", "messaging", "deployment", "nemoclaw"]
tags: ["openclaw", "openshell", "telegram", "discord", "slack", "wechat", "messaging", "deployment", "nemoclaw"]
content:
type: how_to
difficulty: intermediate
Expand All @@ -24,13 +26,14 @@ status: published

# Messaging Channels

Telegram, Discord, and Slack reach your agent through OpenShell-managed processes and gateway constructs.
Telegram, Discord, Slack, and WeChat reach your agent through OpenShell-managed processes and gateway constructs.
NemoClaw registers channel tokens with OpenShell providers, bakes the selected channel configuration into the sandbox image, and keeps runtime delivery under OpenShell control.

You can enable channels during `nemoclaw onboard` or add them later with host-side `nemoclaw <sandbox> channels` commands.
Do not run `openclaw channels add` or `openclaw channels remove` inside the sandbox because `/sandbox/.openclaw/openclaw.json` is generated at image build time and changes inside the running container do not persist across rebuilds.
WeChat works through the same channel commands, with one exception that the iLink QR handshake requires an interactive terminal — see [Add Channels After Onboarding](#add-channels-after-onboarding) for the details.
Do not run `openclaw channels add` or `openclaw channels remove` inside the sandbox because the image build generates `/sandbox/.openclaw/openclaw.json` at build time and changes inside the running container do not persist across rebuilds.

`nemoclaw tunnel start` does not start Telegram, Discord, Slack, or other chat bridges.
`nemoclaw tunnel start` does not start Telegram, Discord, Slack, WeChat, or other chat bridges.
It only starts optional host services such as the cloudflared tunnel when that binary is present. (`nemoclaw start` is kept as a deprecated alias.)
For details, refer to [Commands](../reference/commands.md).

Expand All @@ -47,6 +50,7 @@ For details, refer to [Commands](../reference/commands.md).
| Telegram | `TELEGRAM_BOT_TOKEN` | `TELEGRAM_ALLOWED_IDS` for DM allowlisting, `TELEGRAM_REQUIRE_MENTION` for group-chat replies |
| Discord | `DISCORD_BOT_TOKEN` | `DISCORD_SERVER_ID`, `DISCORD_USER_ID`, `DISCORD_REQUIRE_MENTION` |
| Slack | `SLACK_BOT_TOKEN`, `SLACK_APP_TOKEN` | None |
| WeChat (personal) | Host-side QR scan during `nemoclaw onboard` captures the token — no token to paste | `WECHAT_ALLOWED_IDS` for DM allowlisting (NemoClaw adds the WeChat user who scanned the QR automatically) |

Telegram uses a bot token from [BotFather](https://t.me/BotFather).
Open Telegram, send `/newbot` to [@BotFather](https://t.me/BotFather), follow the prompts, and copy the token.
Expand All @@ -64,13 +68,28 @@ Set `DISCORD_USER_ID` to restrict access to one user; otherwise, any member of t
Slack uses Socket Mode and requires two tokens.
Use `SLACK_BOT_TOKEN` for the bot user OAuth token (`xoxb-...`) and `SLACK_APP_TOKEN` for the app-level Socket Mode token (`xapp-...`).

WeChat delivers messages over Tencent's iLink gateway via the upstream `@tencent-weixin/openclaw-weixin` plugin, baked into the sandbox base image.
The supported mode in this release is **personal WeChat** (`bot_type=3`).
WeChat Official Account and WeCom/Enterprise WeChat are not wired up yet.
Because the bot token only exists after a successful iLink QR handshake, NemoClaw runs the QR login on the host during `nemoclaw onboard`.
You scan the QR with WeChat on your phone (Discover → Scan), confirm the login, and NemoClaw captures the token, `accountId`, `baseUrl`, and `userId` from the iLink response.
NemoClaw registers the token as the `<sandbox>-wechat-bridge` OpenShell provider and substitutes the `openshell:resolve:env:WECHAT_BOT_TOKEN` placeholder for it inside the sandbox, so the token never lands in the image or on disk inside the running container.
WeChat is DM-only (`allowIdsMode: "dm"`) — NemoClaw adds the operator who scanned the QR to `WECHAT_ALLOWED_IDS` automatically, and you can append more comma-separated WeChat user IDs through the same env var.
You can silence the host-side `[wechat]` diagnostic lines (poll status, IDC redirects, swallowed gateway errors) by exporting `NEMOCLAW_WECHAT_QUIET=1` once the flow is stable in your environment.
Tencent's iLink gateway is a third-party service.
Review your organization's terms-of-service, compliance, and data-residency constraints before enabling WeChat in production.

## Enable Channels During Onboarding

When the wizard reaches **Messaging channels**, it lists Telegram, Discord, and Slack.
When the wizard reaches **Messaging channels**, it lists Telegram, Discord, Slack, and WeChat.
Press a channel number to toggle it on or off, then press **Enter** when done.
If a token is not already in the environment or credential store, the wizard prompts for it and saves it.
NemoClaw also selects the matching network policy preset during policy setup so the channel can reach its provider API.

If you enable WeChat, the wizard does not prompt for a paste token.
Instead, it renders a QR code in your terminal, polls Tencent's iLink gateway, and captures the bot token after you scan the QR with WeChat on your phone.
The login has an eight-minute deadline, refreshes the QR up to three times on expiry, and follows iLink's IDC redirects automatically — keep the terminal in the foreground until you see `✓ WeChat login confirmed`.

For scripted setup, export the credentials and optional settings for the channels you want to enable before you run onboarding:

```console
Expand All @@ -82,13 +101,16 @@ $ export SLACK_BOT_TOKEN=<your-slack-bot-token>
$ export SLACK_APP_TOKEN=<your-slack-app-token>
```

This release does not support non-interactive WeChat configuration because the iLink QR handshake requires a human to scan the QR on a paired phone.
Run `nemoclaw onboard` interactively when you want to enable WeChat.

Then run onboarding:

```console
$ nemoclaw onboard
```

Complete the rest of the wizard so the blueprint can create OpenShell providers (for example `<sandbox>-telegram-bridge`), bake channel configuration into the image (`NEMOCLAW_MESSAGING_CHANNELS_B64`), and start the sandbox.
Complete the rest of the wizard so the blueprint can create OpenShell providers (for example `<sandbox>-telegram-bridge`, `<sandbox>-wechat-bridge`), bake channel configuration into the image (`NEMOCLAW_MESSAGING_CHANNELS_B64`), and start the sandbox.

## Add Channels After Onboarding

Expand All @@ -105,6 +127,7 @@ Add the channel you want:
$ nemoclaw my-assistant channels add telegram
$ nemoclaw my-assistant channels add discord
$ nemoclaw my-assistant channels add slack
$ nemoclaw my-assistant channels add wechat
```

`channels add` prompts for missing credentials, registers the bridge with the OpenShell gateway, updates the sandbox registry, and asks whether to rebuild immediately.
Expand Down Expand Up @@ -136,27 +159,58 @@ $ DISCORD_BOT_TOKEN=<your-discord-bot-token> \
nemoclaw my-assistant channels add discord
```

### `channels add wechat`

`channels add wechat` follows the same shape as the other channels with two differences driven by the iLink QR handshake.

First, the command does not prompt for a paste token.
Instead, it renders a QR code in your terminal, polls Tencent's iLink gateway, and captures both the bot token and the per-account metadata (`accountId`, `baseUrl`, `userId`) once you scan the QR with WeChat on your phone (Discover → Scan).
The login has an eight-minute deadline and refreshes the QR up to three times on expiry; keep the terminal in the foreground until you see `✓ WeChat login confirmed`.

Second, the command requires an interactive terminal.
Non-interactive mode (`NEMOCLAW_NON_INTERACTIVE=1`) fails fast with a clear error because the QR handshake needs a paired phone.

```console
$ nemoclaw my-assistant channels add wechat
```

If `WECHAT_BOT_TOKEN` is already cached for this sandbox (the operator onboarded with WeChat earlier), `channels add wechat` reuses the cached token and skips the QR scan to keep the upstream plugin's existing iLink session intact.
Re-running QR would invalidate that session; use `channels remove wechat` first if you intend to acquire a fresh account.

## Rotate or Remove Credentials

Running `channels add` for a channel that is already configured overwrites the stored tokens and registers the updated bridge provider.
For WeChat the cached-token short-circuit applies; see [`channels add wechat`](#channels-add-wechat) for how to acquire a fresh account.
Rebuild the sandbox after the update so the image reflects the current channel set.

To remove a channel and clear its stored credentials, run:

```console
$ nemoclaw my-assistant channels remove telegram
$ nemoclaw my-assistant channels remove wechat
```

`channels remove wechat` clears the bot token, deletes the `<sandbox>-wechat-bridge` OpenShell provider, and drops wechat from the sandbox's enabled-channel set.
The next rebuild produces an image without the wechat channel block in `openclaw.json` and without the per-account state files under `/sandbox/.openclaw/openclaw-weixin/`.

Use `channels stop` when you want to pause a bridge without deleting credentials:

```console
$ nemoclaw my-assistant channels stop telegram
$ nemoclaw my-assistant channels start telegram

$ nemoclaw my-assistant channels stop wechat
$ nemoclaw my-assistant channels start wechat
```

Telegram, Discord, and Slack each allow only one active consumer per channel credential.
For WeChat specifically, `channels stop wechat` followed by a rebuild keeps the per-account state files under `/sandbox/.openclaw/openclaw-weixin/accounts/` intact even though the bridge is no longer wired up in `openclaw.json`.
A subsequent `channels start wechat` + rebuild revives the bridge against the same iLink account without a fresh QR scan.
The bot token is held by the OpenShell provider across the stop/start cycle.

Telegram, Discord, Slack, and WeChat each allow only one active consumer per channel credential.
Multiple sandboxes can use the same channel type at the same time when each sandbox uses a distinct bot/app token.
For example, two Telegram sandboxes can DM the same `TELEGRAM_ALLOWED_IDS` account as long as they use different `TELEGRAM_BOT_TOKEN` values.
For WeChat, each sandbox must own a distinct iLink `accountId` (bot identity) — running two sandboxes against the same WeChat account causes one of them to lose messages.
If you enable a messaging channel and another sandbox already uses the same token, onboarding prompts you to confirm before continuing in interactive mode and exits non-zero in non-interactive mode.
If NemoClaw only has legacy channel metadata and cannot compare credential hashes, it keeps the conservative warning; re-run `channels add <channel>` with the intended token to refresh the stored non-secret hash.
`nemoclaw status` reports cross-sandbox overlaps so you can resolve duplicates before messages start dropping.
Expand All @@ -165,13 +219,14 @@ If NemoClaw only has legacy channel metadata and cannot compare credential hashe

Use `channels stop` when you want to pause one bridge and keep the sandbox running.
Use `nemoclaw tunnel stop` or its deprecated alias `nemoclaw stop` when you want to stop host auxiliary services and also ask NemoClaw to stop the OpenClaw gateway inside the selected sandbox.
Stopping the in-sandbox gateway stops Telegram, Discord, and Slack polling for that sandbox until you restart the sandbox or gateway.
Stopping the in-sandbox gateway stops Telegram, Discord, Slack, and WeChat polling for that sandbox until you restart the sandbox or gateway.

## Confirm Delivery

After the sandbox is running, send a message to the configured bot or app.
If delivery fails, use `openshell term` on the host, check gateway logs, and verify network policy allows the channel API.
Use the matching policy preset (`telegram`, `discord`, or `slack`) or review [Common Integration Policy Examples](../network-policy/integration-policy-examples.md).
Use the matching policy preset (`telegram`, `discord`, `slack`, or `wechat`) or review [Common Integration Policy Examples](../network-policy/integration-policy-examples.md).
For WeChat specifically, the in-sandbox bridge emits a single `[wechat] [<accountId>] provider ready` line on stderr after the first successful iLink hit and an annotated line when the agent turn fails after the provider connected; the diagnostics preload produces both lines, which help you tell "channel up, inference broken" apart from "channel never connected".

## Tunnel Command

Expand Down
Loading
Loading