From ac58dc2e929cbddc18570d1c3c6ecdf01dfd29a8 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 2 May 2026 04:28:57 +0100 Subject: [PATCH] fix(doctor): warn on missing channel env tokens --- CHANGELOG.md | 1 + docs/cli/doctor.md | 1 + docs/install/migrating.md | 8 +++ extensions/discord/src/doctor.test.ts | 41 +++++++++++++++ extensions/discord/src/doctor.ts | 28 ++++++++++- extensions/telegram/src/doctor.test.ts | 69 +++++++++++++++++++++++++- extensions/telegram/src/doctor.ts | 29 ++++++++++- src/channels/plugins/types.adapters.ts | 1 + 8 files changed, 173 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5decd90e643..0da65063436 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Cron: make scheduler reload schedule comparison tolerate malformed persisted jobs, so one bad cron entry no longer aborts the whole tick. Fixes #75886. Thanks @samfox-ai. +- Doctor/channels: warn after migrations when default Telegram or Discord accounts have no configured token and their env fallback (`TELEGRAM_BOT_TOKEN` or `DISCORD_BOT_TOKEN`) is unavailable, with secret-safe migration docs for checking state-dir `.env`. Fixes #74298. Thanks @lolaopenclaw. - Control UI/chat: keep live replies visible when a raw session alias such as `main` sends the chat turn but Gateway emits events under the canonical session key for the same run. Fixes #73716. Thanks @teebes. - CLI/models: reject `--agent` on `openclaw models set` and `set-image` instead of silently writing agent-scoped requests to global model defaults. Fixes #68391. Thanks @derrickabellard. - CLI: stop treating the legacy singular `openclaw tool ...` token as a plugin id under restrictive `plugins.allow`, so it falls through as a normal unknown/reserved command instead of suggesting a stale allowlist entry. Fixes #64732. Thanks @efe-arv, @SweetSophia, and @hashtag1974. diff --git a/docs/cli/doctor.md b/docs/cli/doctor.md index e63933e443c..2c46febd76b 100644 --- a/docs/cli/doctor.md +++ b/docs/cli/doctor.md @@ -58,6 +58,7 @@ Notes: - If sandbox mode is enabled but Docker is unavailable, doctor reports a high-signal warning with remediation (`install Docker` or `openclaw config set agents.defaults.sandbox.mode off`). - If `gateway.auth.token`/`gateway.auth.password` are SecretRef-managed and unavailable in the current command path, doctor reports a read-only warning and does not write plaintext fallback credentials. - If channel SecretRef inspection fails in a fix path, doctor continues and reports a warning instead of exiting early. +- After state-directory migrations, doctor warns when enabled default Telegram or Discord accounts depend on env fallback and `TELEGRAM_BOT_TOKEN` or `DISCORD_BOT_TOKEN` is unavailable to the doctor process. - Telegram `allowFrom` username auto-resolution (`doctor --fix`) requires a resolvable Telegram token in the current command path. If token inspection is unavailable, doctor reports a warning and skips auto-resolution for that pass. ## macOS: `launchctl` env overrides diff --git a/docs/install/migrating.md b/docs/install/migrating.md index 64a8418f29f..a85d3f1fc36 100644 --- a/docs/install/migrating.md +++ b/docs/install/migrating.md @@ -82,6 +82,14 @@ Run `openclaw status` on the old machine to confirm your state directory path. C +If Telegram or Discord uses the default env fallback (`TELEGRAM_BOT_TOKEN` or `DISCORD_BOT_TOKEN`), verify the migrated state-dir `.env` contains those keys without printing the secret values: + +```bash +awk -F= '/^(TELEGRAM_BOT_TOKEN|DISCORD_BOT_TOKEN)=/ { print $1 "=present" }' ~/.openclaw/.env +``` + +`openclaw doctor` also warns when an enabled default Telegram or Discord account has no configured token and the matching env variable is unavailable to the doctor process. + ### Common pitfalls diff --git a/extensions/discord/src/doctor.test.ts b/extensions/discord/src/doctor.test.ts index 17018f5b699..7386768a649 100644 --- a/extensions/discord/src/doctor.test.ts +++ b/extensions/discord/src/doctor.test.ts @@ -1,6 +1,7 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types"; import { describe, expect, it } from "vitest"; import { + collectDiscordMissingEnvTokenWarnings, collectDiscordNumericIdWarnings, discordDoctor, maybeRepairDiscordNumericIds, @@ -361,4 +362,44 @@ describe("discord doctor", () => { expect(warnings[0]).toContain("cannot be auto-repaired"); expect(warnings[1]).toContain("openclaw doctor --fix"); }); + + it("warns when default env fallback token is missing after migration", async () => { + const cfg = { + channels: { + discord: { + allowFrom: ["123"], + }, + }, + } as unknown as OpenClawConfig; + + expect(collectDiscordMissingEnvTokenWarnings({ cfg, env: {} })).toEqual([ + expect.stringContaining("DISCORD_BOT_TOKEN is absent"), + ]); + expect( + collectDiscordMissingEnvTokenWarnings({ cfg, env: { DISCORD_BOT_TOKEN: "Bot tok" } }), + ).toEqual([]); + expect( + await discordDoctor.collectPreviewWarnings?.({ + cfg, + doctorFixCommand: "openclaw doctor --fix", + env: {}, + }), + ).toEqual([expect.stringContaining("DISCORD_BOT_TOKEN is absent")]); + }); + + it("does not warn about DISCORD_BOT_TOKEN when a non-default account is selected", () => { + const cfg = { + channels: { + discord: { + accounts: { + work: { + token: "Bot work-token", + }, + }, + }, + }, + } as unknown as OpenClawConfig; + + expect(collectDiscordMissingEnvTokenWarnings({ cfg, env: {} })).toEqual([]); + }); }); diff --git a/extensions/discord/src/doctor.ts b/extensions/discord/src/doctor.ts index 4beca38d87b..ee8e1df7202 100644 --- a/extensions/discord/src/doctor.ts +++ b/extensions/discord/src/doctor.ts @@ -2,6 +2,8 @@ import { type ChannelDoctorAdapter } from "openclaw/plugin-sdk/channel-contract" import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types"; import { collectProviderDangerousNameMatchingScopes } from "openclaw/plugin-sdk/runtime-doctor"; import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; +import { inspectDiscordAccount } from "./account-inspect.js"; +import { resolveDefaultDiscordAccountId } from "./accounts.js"; import { normalizeCompatibilityConfig as normalizeDiscordCompatibilityConfig } from "./doctor-contract.js"; import { DISCORD_LEGACY_CONFIG_RULES } from "./doctor-shared.js"; import { isDiscordMutableAllowEntry } from "./security-doctor.js"; @@ -235,6 +237,26 @@ export function maybeRepairDiscordNumericIds( }; } +export function collectDiscordMissingEnvTokenWarnings(params: { + cfg: OpenClawConfig; + env?: NodeJS.ProcessEnv; +}): string[] { + if (resolveDefaultDiscordAccountId(params.cfg) !== "default") { + return []; + } + const account = inspectDiscordAccount({ + cfg: params.cfg, + accountId: "default", + envToken: params.env?.DISCORD_BOT_TOKEN ?? "", + }); + if (!account.enabled || account.tokenStatus !== "missing" || account.tokenSource !== "none") { + return []; + } + return [ + "- channels.discord: default account has no available bot token, and DISCORD_BOT_TOKEN is absent in this doctor environment. After migration, verify DISCORD_BOT_TOKEN is present in the state-dir .env or configure channels.discord.token / channels.discord.accounts.default.token as a SecretRef.", + ]; +} + function collectDiscordMutableAllowlistWarnings(cfg: OpenClawConfig): string[] { const hits: Array<{ path: string; entry: string }> = []; const addHits = (pathLabel: string, list: unknown) => { @@ -306,11 +328,13 @@ export const discordDoctor: ChannelDoctorAdapter = { warnOnEmptyGroupSenderAllowlist: false, legacyConfigRules: DISCORD_LEGACY_CONFIG_RULES, normalizeCompatibilityConfig: normalizeDiscordCompatibilityConfig, - collectPreviewWarnings: ({ cfg, doctorFixCommand }) => - collectDiscordNumericIdWarnings({ + collectPreviewWarnings: ({ cfg, doctorFixCommand, env }) => [ + ...collectDiscordMissingEnvTokenWarnings({ cfg, env }), + ...collectDiscordNumericIdWarnings({ hits: scanDiscordNumericIdEntries(cfg), doctorFixCommand, }), + ], collectMutableAllowlistWarnings: ({ cfg }) => collectDiscordMutableAllowlistWarnings(cfg), repairConfig: ({ cfg, doctorFixCommand }) => maybeRepairDiscordNumericIds(cfg, doctorFixCommand), }; diff --git a/extensions/telegram/src/doctor.test.ts b/extensions/telegram/src/doctor.test.ts index e2e6e54d392..6fffb881950 100644 --- a/extensions/telegram/src/doctor.test.ts +++ b/extensions/telegram/src/doctor.test.ts @@ -5,6 +5,7 @@ import { collectTelegramApiRootWarnings, collectTelegramEmptyAllowlistExtraWarnings, collectTelegramGroupPolicyWarnings, + collectTelegramMissingEnvTokenWarnings, maybeRepairTelegramApiRoots, maybeRepairTelegramAllowFromUsernames, scanTelegramBotEndpointApiRoots, @@ -62,7 +63,7 @@ describe("telegram doctor", () => { enabled: true, token: "tok", tokenSource: "config", - tokenStatus: "configured", + tokenStatus: "available", }); lookupTelegramChatIdMock.mockReset(); }); @@ -355,4 +356,70 @@ describe("telegram doctor", () => { "- channels.telegram.apiRoot: removed trailing /bot from Telegram apiRoot.", ]); }); + + it("warns when default env fallback token is missing after migration", async () => { + const cfg = { + channels: { + telegram: { + allowFrom: ["123"], + }, + }, + } as unknown as OpenClawConfig; + + inspectTelegramAccountMock.mockReturnValueOnce({ + enabled: true, + token: "", + tokenSource: "none", + tokenStatus: "missing", + configured: false, + config: {}, + }); + expect(collectTelegramMissingEnvTokenWarnings({ cfg, env: {} })).toEqual([ + expect.stringContaining("TELEGRAM_BOT_TOKEN is absent"), + ]); + + inspectTelegramAccountMock.mockReturnValueOnce({ + enabled: true, + token: "123:tok", + tokenSource: "env", + tokenStatus: "available", + configured: true, + config: {}, + }); + expect( + collectTelegramMissingEnvTokenWarnings({ cfg, env: { TELEGRAM_BOT_TOKEN: "123:tok" } }), + ).toEqual([]); + + inspectTelegramAccountMock.mockReturnValueOnce({ + enabled: true, + token: "", + tokenSource: "none", + tokenStatus: "missing", + configured: false, + config: {}, + }); + expect( + await telegramDoctor.collectPreviewWarnings?.({ + cfg, + doctorFixCommand: "openclaw doctor --fix", + env: {}, + }), + ).toContainEqual(expect.stringContaining("TELEGRAM_BOT_TOKEN is absent")); + }); + + it("does not warn about TELEGRAM_BOT_TOKEN when a non-default account is selected", () => { + const cfg = { + channels: { + telegram: { + accounts: { + work: { + botToken: "123:work", + }, + }, + }, + }, + } as unknown as OpenClawConfig; + + expect(collectTelegramMissingEnvTokenWarnings({ cfg, env: {} })).toEqual([]); + }); }); diff --git a/extensions/telegram/src/doctor.ts b/extensions/telegram/src/doctor.ts index 8cc18ad79bb..a197450df2f 100644 --- a/extensions/telegram/src/doctor.ts +++ b/extensions/telegram/src/doctor.ts @@ -6,7 +6,11 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; import { inspectTelegramAccount } from "./account-inspect.js"; -import { listTelegramAccountIds, resolveTelegramAccount } from "./accounts.js"; +import { + listTelegramAccountIds, + resolveDefaultTelegramAccountId, + resolveTelegramAccount, +} from "./accounts.js"; import { isNumericTelegramSenderUserId, normalizeTelegramAllowFromEntry } from "./allow-from.js"; import { lookupTelegramChatId } from "./api-fetch.js"; import { hasTelegramBotEndpointApiRoot, normalizeTelegramApiRoot } from "./api-root.js"; @@ -224,6 +228,26 @@ export function maybeRepairTelegramApiRoots(cfg: OpenClawConfig): { }; } +export function collectTelegramMissingEnvTokenWarnings(params: { + cfg: OpenClawConfig; + env?: NodeJS.ProcessEnv; +}): string[] { + if (resolveDefaultTelegramAccountId(params.cfg) !== "default") { + return []; + } + const account = inspectTelegramAccount({ + cfg: params.cfg, + accountId: "default", + envToken: params.env?.TELEGRAM_BOT_TOKEN ?? "", + }); + if (!account.enabled || account.tokenStatus !== "missing" || account.tokenSource !== "none") { + return []; + } + return [ + "- channels.telegram: default account has no available bot token, and TELEGRAM_BOT_TOKEN is absent in this doctor environment. After migration, verify TELEGRAM_BOT_TOKEN is present in the state-dir .env or configure channels.telegram.botToken / channels.telegram.accounts.default.botToken as a SecretRef.", + ]; +} + async function repairTelegramConfig(params: { cfg: OpenClawConfig }): Promise<{ config: OpenClawConfig; changes: string[]; @@ -472,7 +496,8 @@ export function collectTelegramEmptyAllowlistExtraWarnings( export const telegramDoctor: ChannelDoctorAdapter = { legacyConfigRules: TELEGRAM_LEGACY_CONFIG_RULES, normalizeCompatibilityConfig: normalizeTelegramCompatibilityConfig, - collectPreviewWarnings: ({ cfg, doctorFixCommand }) => [ + collectPreviewWarnings: ({ cfg, doctorFixCommand, env }) => [ + ...collectTelegramMissingEnvTokenWarnings({ cfg, env }), ...collectTelegramInvalidAllowFromWarnings({ hits: scanTelegramInvalidAllowFromEntries(cfg), doctorFixCommand, diff --git a/src/channels/plugins/types.adapters.ts b/src/channels/plugins/types.adapters.ts index 4592ee226d1..b7ec912c993 100644 --- a/src/channels/plugins/types.adapters.ts +++ b/src/channels/plugins/types.adapters.ts @@ -524,6 +524,7 @@ export type ChannelDoctorAdapter = { collectPreviewWarnings?: (params: { cfg: OpenClawConfig; doctorFixCommand: string; + env?: NodeJS.ProcessEnv; }) => string[] | Promise; collectMutableAllowlistWarnings?: (params: { cfg: OpenClawConfig;