fix(doctor): warn on missing channel env tokens

This commit is contained in:
Peter Steinberger
2026-05-02 04:28:57 +01:00
parent d2f623d560
commit ac58dc2e92
8 changed files with 173 additions and 5 deletions

View File

@@ -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.

View File

@@ -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

View File

@@ -82,6 +82,14 @@ Run `openclaw status` on the old machine to confirm your state directory path. C
</Step>
</Steps>
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
<AccordionGroup>

View File

@@ -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([]);
});
});

View File

@@ -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),
};

View File

@@ -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<TOKEN> 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([]);
});
});

View File

@@ -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,

View File

@@ -524,6 +524,7 @@ export type ChannelDoctorAdapter = {
collectPreviewWarnings?: (params: {
cfg: OpenClawConfig;
doctorFixCommand: string;
env?: NodeJS.ProcessEnv;
}) => string[] | Promise<string[]>;
collectMutableAllowlistWarnings?: (params: {
cfg: OpenClawConfig;