Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion docs/_components/StarterPromptButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ Channel credential requirements:

| Channel | Required values |
|---|---|
| Telegram | \`TELEGRAM_BOT_TOKEN\`; optional \`TELEGRAM_ALLOWED_IDS\`, \`TELEGRAM_REQUIRE_MENTION\` |
| Telegram | \`TELEGRAM_BOT_TOKEN\`; optional \`TELEGRAM_ALLOWED_IDS\`, \`TELEGRAM_REQUIRE_MENTION\`, \`TELEGRAM_GROUP_POLICY\` (OpenClaw only) |
| Discord | \`DISCORD_BOT_TOKEN\`; optional \`DISCORD_SERVER_ID\`, \`DISCORD_USER_ID\`, \`DISCORD_REQUIRE_MENTION\` |
| Slack | \`SLACK_BOT_TOKEN\`, \`SLACK_APP_TOKEN\`; optional \`SLACK_ALLOWED_USERS\`, \`SLACK_ALLOWED_CHANNELS\` |
| WhatsApp | No host token; add the channel, rebuild, then complete QR pairing inside the sandbox as documented |
Expand Down
15 changes: 12 additions & 3 deletions docs/manage-sandboxes/messaging-channels.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ For details, refer to [Commands](../reference/commands).

| Channel | Required tokens | Optional settings |
|---------|-----------------|-------------------|
| Telegram | `TELEGRAM_BOT_TOKEN` | `TELEGRAM_ALLOWED_IDS` for DM allowlisting, `TELEGRAM_REQUIRE_MENTION` for group-chat replies |
| Telegram | `TELEGRAM_BOT_TOKEN` | `TELEGRAM_ALLOWED_IDS` for DM allowlisting, `TELEGRAM_REQUIRE_MENTION` for group-chat replies, `TELEGRAM_GROUP_POLICY` for OpenClaw group access |
| Discord | `DISCORD_BOT_TOKEN` | `DISCORD_SERVER_ID`, `DISCORD_USER_ID`, `DISCORD_REQUIRE_MENTION` |
| Slack | `SLACK_BOT_TOKEN`, `SLACK_APP_TOKEN` | `SLACK_ALLOWED_USERS` for DM and channel `@mention` user allowlisting, `SLACK_ALLOWED_CHANNELS` for channel ID allowlisting |
| WeChat (experimental) | None. Captured through host-side QR scan during `$$nemoclaw onboard` | `WECHAT_ALLOWED_IDS` for DM allowlisting |
Expand All @@ -68,12 +68,17 @@ After changing privacy mode, remove the bot from each Telegram group and add it
For compatibility with older QA scripts, NemoClaw also treats `TELEGRAM_AUTHORIZED_CHAT_IDS` and `TELEGRAM_CHAT_ID` as aliases, but new automation should use `TELEGRAM_ALLOWED_IDS`.
Keep these aliases until QA automation and public repro templates have stopped exporting them for at least one full release.
Group chats stay open by default so rebuilt sandboxes do not silently drop Telegram group messages because of an empty group allowlist.
Set `TELEGRAM_REQUIRE_MENTION=1` to make the bot reply in Telegram groups only when users mention it.
NemoClaw defaults `TELEGRAM_GROUP_POLICY` to `open` for OpenClaw.
Set `TELEGRAM_GROUP_POLICY=allowlist` to block OpenClaw group chats until explicit group entries are configured, or `TELEGRAM_GROUP_POLICY=disabled` to disable OpenClaw Telegram group access.
Hermes does not have an equivalent disable-groups policy; `TELEGRAM_ALLOWED_IDS` maps to Hermes `TELEGRAM_ALLOWED_USERS`, which authorizes those users across DMs, groups, and forums.
NemoClaw defaults `TELEGRAM_REQUIRE_MENTION` to `1`, so the bot replies in Telegram groups only when users mention it.
Set `TELEGRAM_REQUIRE_MENTION=0` when you want the bot to reply to all group messages.
Pairing and `TELEGRAM_ALLOWED_IDS` still govern direct messages.

Discord uses a bot token from the Discord Developer Portal.
For server channels, enable Developer Mode in Discord, right-click the server, and copy the Server ID into `DISCORD_SERVER_ID`.
By default, NemoClaw configures the bot to reply only when mentioned.
If `DISCORD_SERVER_ID` is set and `DISCORD_REQUIRE_MENTION` is unset, NemoClaw defaults `DISCORD_REQUIRE_MENTION` to `1`.
Set `DISCORD_REQUIRE_MENTION=0` if you want it to reply to all messages in the configured server.
Set `DISCORD_USER_ID` to restrict access to one user; otherwise, any member of the configured server can message the bot.

Expand Down Expand Up @@ -201,7 +206,9 @@ Verify the gateway bridge before relying on the channel.
Restore the preset YAML and re-run `$$nemoclaw <sandbox> channels add <channel>`.
Choose the rebuild so the running sandbox image picks up the new channel.
For Telegram, Discord, and Slack, `channels add` also checks the rebuilt runtime for the selected bridge and reports startup, credential, or missing-plugin warnings before returning.
If you need optional channel settings such as `TELEGRAM_ALLOWED_IDS`, `TELEGRAM_REQUIRE_MENTION`, `DISCORD_SERVER_ID`, `DISCORD_USER_ID`, `DISCORD_REQUIRE_MENTION`, `SLACK_ALLOWED_USERS`, or `SLACK_ALLOWED_CHANNELS`, export them before the rebuild starts.
If you need optional channel settings such as `TELEGRAM_ALLOWED_IDS`, `TELEGRAM_REQUIRE_MENTION`, `TELEGRAM_GROUP_POLICY`, `DISCORD_SERVER_ID`, `DISCORD_USER_ID`, `DISCORD_REQUIRE_MENTION`, `SLACK_ALLOWED_USERS`, or `SLACK_ALLOWED_CHANNELS`, export them before the rebuild starts.
You can omit `TELEGRAM_REQUIRE_MENTION` and `DISCORD_REQUIRE_MENTION` when you want the default mention-only mode.
You can omit `TELEGRAM_GROUP_POLICY` when you want OpenClaw Telegram group access to stay open.
Telegram Bot API `sendMessage` calls prove outbound delivery from the bot; to test inbound agent replies, send a message from the Telegram client as an allowed user.
For a repeatable live Telegram reply check, run `test/e2e/test-messaging-providers.sh` with `TELEGRAM_BOT_TOKEN_REAL`, `TELEGRAM_AUTHORIZED_CHAT_IDS` or `TELEGRAM_CHAT_ID`, and `NEMOCLAW_TELEGRAM_INBOUND_REPLY_E2E=1`.
If you defer the rebuild, apply the change later:
Expand All @@ -211,6 +218,8 @@ $$nemoclaw my-assistant rebuild
```

In non-interactive mode, set the required environment variables before running `channels add`.
Optional mention-mode settings that declare defaults are still written when unset.
Telegram mention mode defaults to `1`; Discord mention mode defaults to `1` when `DISCORD_SERVER_ID` is set.
Missing credentials fail fast, and the command queues the change for a manual rebuild:

```bash
Expand Down
1 change: 1 addition & 0 deletions docs/reference/architecture.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ The following environment variables configure optional services and local access
|---|---|
| `TELEGRAM_BOT_TOKEN` | Telegram bot token you provide before `$$nemoclaw onboard`. OpenShell stores it in a provider; the sandbox receives placeholders, not the raw secret. |
| `TELEGRAM_ALLOWED_IDS` | Comma-separated Telegram user or chat IDs for allowlists when onboarding applies channel restrictions. |
| `TELEGRAM_GROUP_POLICY` | OpenClaw Telegram group access policy: `open` by default, `allowlist` to require explicit group entries, or `disabled` to turn off OpenClaw group access. Hermes ignores this value. |
| `SLACK_BOT_TOKEN` | Slack bot token (`xoxb-...`) you provide before `$$nemoclaw onboard`. Stored as an OpenShell provider; never passed directly to the sandbox. |
| `SLACK_APP_TOKEN` | Slack app-level token (`xapp-...`) required for Socket Mode. Stored alongside `SLACK_BOT_TOKEN` during onboarding. |
| `SLACK_ALLOWED_USERS` | Comma-separated Slack member IDs for DM and channel `@mention` user allowlisting. |
Expand Down
8 changes: 7 additions & 1 deletion docs/reference/commands-nemohermes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -207,9 +207,13 @@ If you enable Discord during onboarding, the wizard can also prompt for a Discor
NemoClaw bakes those values into the sandbox image as Discord guild workspace config so the bot can respond in the selected server, not just in DMs.
If you leave the Discord User ID blank, the guild config omits the user allowlist and any member of the configured server can message the bot.
Guild responses remain mention-gated by default unless you opt into all-message replies.
If `DISCORD_SERVER_ID` is set and `DISCORD_REQUIRE_MENTION` is unset, NemoClaw records the existing mention-only default (`DISCORD_REQUIRE_MENTION=1`).

If you enable Telegram during onboarding, the wizard can also prompt for whether group chats should reply only to `@mentions` or to all group messages.
Set `TELEGRAM_REQUIRE_MENTION=1` for non-interactive onboarding when you want mention-only group replies.
Mention-only group replies are the default.
Set `TELEGRAM_REQUIRE_MENTION=0` for non-interactive onboarding when you want all group messages to trigger replies.
For OpenClaw, Telegram group access defaults to `TELEGRAM_GROUP_POLICY=open`; set `TELEGRAM_GROUP_POLICY=allowlist` or `TELEGRAM_GROUP_POLICY=disabled` before non-interactive onboarding when you want stricter group access.
Hermes does not have an equivalent disable-groups policy; `TELEGRAM_ALLOWED_IDS` maps to Hermes `TELEGRAM_ALLOWED_USERS`, which authorizes those users across DMs, groups, and forums.
Pairing and `TELEGRAM_ALLOWED_IDS` still govern direct messages.

If you cancel a brand-new onboarding run at the policy preset step, NemoClaw rolls back the sandbox, registry entry, and onboarding session instead of leaving a default sandbox with unfinished policy state.
Expand Down Expand Up @@ -880,6 +884,8 @@ nemohermes my-assistant channels add telegram

Slack requires both `SLACK_BOT_TOKEN` (bot user OAuth) and `SLACK_APP_TOKEN` (app-level Socket Mode token); the command prompts for each in turn.
Optional Slack allowlists come from `SLACK_ALLOWED_USERS` and `SLACK_ALLOWED_CHANNELS` at rebuild time.
Telegram and Discord mention mode default to `1` when no environment, session, or saved state value exists for that setting.
Discord applies that default only when a server ID is configured.
When `NEMOCLAW_NON_INTERACTIVE=1` is set, any missing token fails fast and no rebuild prompt is shown — instead, the change is queued and you are told to run `nemohermes <name> rebuild` manually.
If you omit the required `<channel>` argument, the CLI prints the `channels add <channel>` usage with the supported channel list instead of falling back to top-level help.

Expand Down
8 changes: 7 additions & 1 deletion docs/reference/commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -289,9 +289,13 @@ If you enable Discord during onboarding, the wizard can also prompt for a Discor
NemoClaw bakes those values into the sandbox image as Discord guild workspace config so the bot can respond in the selected server, not just in DMs.
If you leave the Discord User ID blank, the guild config omits the user allowlist and any member of the configured server can message the bot.
Guild responses remain mention-gated by default unless you opt into all-message replies.
If `DISCORD_SERVER_ID` is set and `DISCORD_REQUIRE_MENTION` is unset, NemoClaw records the existing mention-only default (`DISCORD_REQUIRE_MENTION=1`).

If you enable Telegram during onboarding, the wizard can also prompt for whether group chats should reply only to `@mentions` or to all group messages.
Set `TELEGRAM_REQUIRE_MENTION=1` for non-interactive onboarding when you want mention-only group replies.
Mention-only group replies are the default.
Set `TELEGRAM_REQUIRE_MENTION=0` for non-interactive onboarding when you want all group messages to trigger replies.
For OpenClaw, Telegram group access defaults to `TELEGRAM_GROUP_POLICY=open`; set `TELEGRAM_GROUP_POLICY=allowlist` or `TELEGRAM_GROUP_POLICY=disabled` before non-interactive onboarding when you want stricter group access.
Hermes does not have an equivalent disable-groups policy; `TELEGRAM_ALLOWED_IDS` maps to Hermes `TELEGRAM_ALLOWED_USERS`, which authorizes those users across DMs, groups, and forums.
Pairing and `TELEGRAM_ALLOWED_IDS` still govern direct messages.

If you cancel a brand-new onboarding run at the policy preset step, NemoClaw rolls back the sandbox, registry entry, and onboarding session instead of leaving a default sandbox with unfinished policy state.
Expand Down Expand Up @@ -1126,6 +1130,8 @@ $$nemoclaw my-assistant channels add telegram

Slack requires both `SLACK_BOT_TOKEN` (bot user OAuth) and `SLACK_APP_TOKEN` (app-level Socket Mode token); the command prompts for each in turn.
Optional Slack allowlists come from `SLACK_ALLOWED_USERS` and `SLACK_ALLOWED_CHANNELS` at rebuild time.
Telegram and Discord mention mode default to `1` when no environment, session, or saved state value exists for that setting.
Discord applies that default only when a server ID is configured.
When `NEMOCLAW_NON_INTERACTIVE=1` is set, any missing token fails fast and no rebuild prompt is shown — instead, the change is queued and you are told to run `$$nemoclaw <name> rebuild` manually.
If you omit the required `<channel>` argument, the CLI prints the `channels add <channel>` usage with the supported channel list instead of falling back to top-level help.

Expand Down
10 changes: 9 additions & 1 deletion src/lib/messaging-channel-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,20 @@ describe("messaging channel config", () => {
"SLACK_ALLOWED_USERS",
"SLACK_ALLOWED_CHANNELS",
"WHATSAPP_ALLOWED_IDS",
"TELEGRAM_GROUP_POLICY",
"WECHAT_ACCOUNT_ID",
"WECHAT_BASE_URL",
"WECHAT_USER_ID",
]);
});

it("sanitizes persisted config and rejects malformed reply-mode values", () => {
it("sanitizes persisted config and rejects malformed choice values", () => {
expect(
sanitizeMessagingChannelConfig({
TELEGRAM_ALLOWED_IDS: " 123,456 ",
TELEGRAM_REQUIRE_MENTION: "yes",
TELEGRAM_GROUP_POLICY: "allowlist",
TELEGRAM_GROUP_POLICY_INVALID: "disabled",
DISCORD_SERVER_ID: "1491590992753590594",
DISCORD_REQUIRE_MENTION: "0",
SLACK_ALLOWED_USERS: " U01ABC2DEF3, U04GHI5JKL6 ",
Expand All @@ -41,6 +44,7 @@ describe("messaging channel config", () => {
}),
).toEqual({
TELEGRAM_ALLOWED_IDS: "123,456",
TELEGRAM_GROUP_POLICY: "allowlist",
DISCORD_SERVER_ID: "1491590992753590594",
DISCORD_REQUIRE_MENTION: "0",
SLACK_ALLOWED_USERS: "U01ABC2DEF3, U04GHI5JKL6",
Expand Down Expand Up @@ -93,6 +97,7 @@ describe("messaging channel config", () => {
{
TELEGRAM_ALLOWED_IDS: "stored-user",
TELEGRAM_REQUIRE_MENTION: "1",
TELEGRAM_GROUP_POLICY: "nonsense",
DISCORD_REQUIRE_MENTION: "maybe",
},
env,
Expand All @@ -103,6 +108,7 @@ describe("messaging channel config", () => {
});
expect(env.TELEGRAM_ALLOWED_IDS).toBe("env-user");
expect(env.TELEGRAM_REQUIRE_MENTION).toBe("1");
expect(env.TELEGRAM_GROUP_POLICY).toBeUndefined();
expect(env.DISCORD_REQUIRE_MENTION).toBeUndefined();
});

Expand All @@ -121,10 +127,12 @@ describe("messaging channel config", () => {
DISCORD_SERVER_ID: "1491590992753590594",
DISCORD_REQUIRE_MENTION: "2",
TELEGRAM_REQUIRE_MENTION: "0",
TELEGRAM_GROUP_POLICY: "disabled",
}),
).toEqual({
DISCORD_SERVER_ID: "1491590992753590594",
TELEGRAM_REQUIRE_MENTION: "0",
TELEGRAM_GROUP_POLICY: "disabled",
});
});
});
26 changes: 16 additions & 10 deletions src/lib/messaging-channel-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,21 @@ const manifestConfigInputs = BUILT_IN_CHANNEL_MANIFESTS.flatMap((manifest) =>
validValues: "validValues" in input ? input.validValues : undefined,
})),
);
const requireMentionKeys = new Set(
[
...channels.map((channel) => channel.requireMentionEnvKey),
...manifestConfigInputs.filter(hasBooleanStringValues).map((input) => input.envKey),
].filter((key): key is string => typeof key === "string" && key.length > 0),
const validValuesByKey = new Map<string, ReadonlySet<string>>(
manifestConfigInputs.flatMap((input) => {
if (
typeof input.envKey !== "string" ||
input.envKey.length === 0 ||
!Array.isArray(input.validValues)
) {
return [];
}
return [[input.envKey, new Set(input.validValues)] as const];
}),
);
for (const key of channels.map((channel) => channel.requireMentionEnvKey)) {
if (key && !validValuesByKey.has(key)) validValuesByKey.set(key, new Set(["0", "1"]));
}

const configKeyAliases = getMessagingConfigEnvAliases();

Expand Down Expand Up @@ -49,10 +58,6 @@ export const MESSAGING_CHANNEL_CONFIG_ENV_KEYS: readonly string[] = [

const knownConfigKeys = new Set(MESSAGING_CHANNEL_CONFIG_ENV_KEYS);

function hasBooleanStringValues(input: { readonly validValues?: readonly string[] }): boolean {
return input.validValues?.includes("0") === true && input.validValues.includes("1");
}

export type MessagingChannelConfigEnvResolution = {
canonicalKey: string | null;
sourceKey: string | null;
Expand Down Expand Up @@ -83,7 +88,8 @@ export function normalizeMessagingChannelConfigValue(key: string, value: unknown
if (!canonical) return null;
const normalized = normalizeValue(value);
if (!normalized) return null;
if (requireMentionKeys.has(canonical) && normalized !== "0" && normalized !== "1") {
const validValues = validValuesByKey.get(canonical);
if (validValues && !validValues.has(normalized)) {
return null;
}
return normalized;
Expand Down
2 changes: 1 addition & 1 deletion src/lib/messaging/applier/setup-applier.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ describe("MessagingSetupApplier", () => {
enabled: true,
groupPolicy: "open",
});
expect(openclawConfig.channels.telegram.groups).toBeUndefined();
expect(openclawConfig.channels.telegram.groups).toEqual({ "*": { requireMention: true } });
expect(result.appliedTargets).toEqual(["/sandbox/.openclaw/openclaw.json"]);
expect(result.appliedHooks).toEqual([]);
expect(result.unresolvedTemplateRefs).toEqual([]);
Expand Down
1 change: 1 addition & 0 deletions src/lib/messaging/channels/discord/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export const discordManifest = {
statePath: "discordGuilds.requireMention",
promptWhenInput: "serverId",
validValues: ["0", "1"],
defaultValue: "1",
prompt: {
label: "Discord mention mode",
help: "Choose whether the bot should reply only when @mentioned or to all messages in this server.",
Expand Down
24 changes: 24 additions & 0 deletions src/lib/messaging/channels/manifests.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,10 +304,19 @@ describe("built-in channel manifests", () => {
const botToken = findInput(telegramManifest, "botToken");
const allowedIds = findInput(telegramManifest, "allowedIds");
const requireMention = findInput(telegramManifest, "requireMention");
const groupPolicy = findInput(telegramManifest, "groupPolicy");
expect(getChannelTokenKeys(KNOWN_CHANNELS.telegram)).toEqual(["TELEGRAM_BOT_TOKEN"]);
expect(botToken.envKey).toBe("TELEGRAM_BOT_TOKEN");
expect(allowedIds.envKey).toBe("TELEGRAM_ALLOWED_IDS");
expect(requireMention.envKey).toBe("TELEGRAM_REQUIRE_MENTION");
expect(requireMention).toMatchObject({ kind: "config", defaultValue: "1" });
expect(groupPolicy).toMatchObject({
kind: "config",
envKey: "TELEGRAM_GROUP_POLICY",
statePath: "telegramConfig.groupPolicy",
defaultValue: "open",
validValues: ["open", "allowlist", "disabled"],
});
expect(KNOWN_CHANNELS.telegram.allowIdsMode).toBe("dm");
expect(telegramManifest.credentials).toEqual([
{
Expand All @@ -327,6 +336,8 @@ describe("built-in channel manifests", () => {
expect(renderJson(telegramManifest)).toContain("groupPolicy");
expect(renderJson(telegramManifest)).toContain("channels.telegram.groups");
expect(renderJson(telegramManifest)).toContain("telegramConfig.requireMention");
expect(renderJson(telegramManifest)).toContain("telegramConfig.groupPolicy");
expect(renderJson(telegramManifest)).toContain("telegramConfig.openclawGroups");
expect(renderJson(telegramManifest)).toContain("platforms.telegram");
expectTokenPasteEnrollHook(telegramManifest, ["botToken"]);
expect(telegramManifest.hooks).toContainEqual({
Expand All @@ -341,6 +352,18 @@ describe("built-in channel manifests", () => {
],
});
expectConfigPromptEnrollHook(telegramManifest, ["requireMention", "allowedIds"]);
expect(telegramManifest.hooks).toContainEqual({
id: "telegram-openclaw-config-prompt",
phase: "enroll",
handler: COMMON_CONFIG_PROMPT_HOOK_HANDLER_ID,
agents: ["openclaw"],
outputs: [
{
id: "groupPolicy",
kind: "config",
},
],
});
expectReachabilityHook(telegramManifest, ["botToken"]);
expectOpenClawNodePreload(telegramManifest, "telegram-diagnostics");
expect(JSON.stringify(telegramManifest.runtime?.openclaw)).toContain("telegram-diagnostics");
Expand All @@ -367,6 +390,7 @@ describe("built-in channel manifests", () => {
expect(botToken.envKey).toBe("DISCORD_BOT_TOKEN");
expect(serverId.envKey).toBe("DISCORD_SERVER_ID");
expect(requireMention.envKey).toBe("DISCORD_REQUIRE_MENTION");
expect(requireMention).toMatchObject({ kind: "config", defaultValue: "1" });
expect(userId.envKey).toBe("DISCORD_USER_ID");
expect(KNOWN_CHANNELS.discord.allowIdsMode).toBe("guild");
expect(discordManifest.credentials).toEqual([
Expand Down
1 change: 1 addition & 0 deletions src/lib/messaging/channels/metadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ describe("built-in messaging channel metadata", () => {
expect(listMessagingConfigEnvKeys()).toEqual([
"TELEGRAM_ALLOWED_IDS",
"TELEGRAM_REQUIRE_MENTION",
"TELEGRAM_GROUP_POLICY",
"DISCORD_SERVER_ID",
"DISCORD_REQUIRE_MENTION",
"DISCORD_USER_ID",
Expand Down
Loading
Loading