From 330579ef9612c89c89c1e22a948dcb581edc8e96 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 7 Mar 2026 20:50:07 +0000 Subject: [PATCH] fix(telegram): resolve status SecretRefs with provider-safe env checks Landed from #39130 by @neocody. Co-authored-by: Cody <25426121+neocody@users.noreply.github.com> --- CHANGELOG.md | 1 + src/telegram/account-inspect.test.ts | 79 ++++++++++++++++++++++++++++ src/telegram/account-inspect.ts | 68 +++++++++++++++++++++--- 3 files changed, 141 insertions(+), 7 deletions(-) create mode 100644 src/telegram/account-inspect.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index e1d6c20cca7..63b76306f5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -275,6 +275,7 @@ Docs: https://docs.openclaw.ai - Gateway/session bootstrap cache invalidation ordering: clear bootstrap snapshots only after active embedded-run shutdown wait completes, preventing dying runs from repopulating stale cache between `/new`/`sessions.reset` turns. (#38873) Thanks @MumuTW. - Browser/dispatcher error clarity: preserve dispatcher-side failure context in browser fetch errors while still appending operator guidance and explicit no-retry model hints, preventing misleading `"Can't reach service"` wrapping and avoiding LLM retry loops. (#39090) Thanks @NewdlDewdl. - Telegram/polling offset safety: confirm persisted offsets before polling startup while validating stored `lastUpdateId` values as non-negative safe integers (with overflow guards) so malformed offset state cannot cause update skipping/dropping. (#39111) Thanks @MumuTW. +- Telegram/status SecretRef read-only resolution: resolve env-backed bot-token SecretRefs in config-only/status inspection while respecting provider source/defaults and env allowlists, so status no longer crashes or reports false-ready tokens for disallowed providers. (#39130) Thanks @neocody. ## 2026.3.2 diff --git a/src/telegram/account-inspect.test.ts b/src/telegram/account-inspect.test.ts new file mode 100644 index 00000000000..83ad113202b --- /dev/null +++ b/src/telegram/account-inspect.test.ts @@ -0,0 +1,79 @@ +import { describe, expect, it } from "vitest"; +import type { OpenClawConfig } from "../config/config.js"; +import { withEnv } from "../test-utils/env.js"; +import { inspectTelegramAccount } from "./account-inspect.js"; + +describe("inspectTelegramAccount SecretRef resolution", () => { + it("resolves default env SecretRef templates in read-only status paths", () => { + withEnv({ TG_STATUS_TOKEN: "123:token" }, () => { + const cfg: OpenClawConfig = { + channels: { + telegram: { + botToken: "${TG_STATUS_TOKEN}", + }, + }, + }; + + const account = inspectTelegramAccount({ cfg, accountId: "default" }); + expect(account.tokenSource).toBe("env"); + expect(account.tokenStatus).toBe("available"); + expect(account.token).toBe("123:token"); + }); + }); + + it("respects env provider allowlists in read-only status paths", () => { + withEnv({ TG_NOT_ALLOWED: "123:token" }, () => { + const cfg: OpenClawConfig = { + secrets: { + defaults: { + env: "secure-env", + }, + providers: { + "secure-env": { + source: "env", + allowlist: ["TG_ALLOWED"], + }, + }, + }, + channels: { + telegram: { + botToken: "${TG_NOT_ALLOWED}", + }, + }, + }; + + const account = inspectTelegramAccount({ cfg, accountId: "default" }); + expect(account.tokenSource).toBe("env"); + expect(account.tokenStatus).toBe("configured_unavailable"); + expect(account.token).toBe(""); + }); + }); + + it("does not read env values for non-env providers", () => { + withEnv({ TG_EXEC_PROVIDER: "123:token" }, () => { + const cfg: OpenClawConfig = { + secrets: { + defaults: { + env: "exec-provider", + }, + providers: { + "exec-provider": { + source: "exec", + command: "/usr/bin/env", + }, + }, + }, + channels: { + telegram: { + botToken: "${TG_EXEC_PROVIDER}", + }, + }, + }; + + const account = inspectTelegramAccount({ cfg, accountId: "default" }); + expect(account.tokenSource).toBe("env"); + expect(account.tokenStatus).toBe("configured_unavailable"); + expect(account.token).toBe(""); + }); + }); +}); diff --git a/src/telegram/account-inspect.ts b/src/telegram/account-inspect.ts index 305e410d39a..0ffbe0281ff 100644 --- a/src/telegram/account-inspect.ts +++ b/src/telegram/account-inspect.ts @@ -1,9 +1,14 @@ import fs from "node:fs"; import type { OpenClawConfig } from "../config/config.js"; -import { hasConfiguredSecretInput, normalizeSecretInputString } from "../config/types.secrets.js"; +import { + coerceSecretRef, + hasConfiguredSecretInput, + normalizeSecretInputString, +} from "../config/types.secrets.js"; import type { TelegramAccountConfig } from "../config/types.telegram.js"; import { resolveAccountWithDefaultFallback } from "../plugin-sdk/account-resolution.js"; import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js"; +import { resolveDefaultSecretProviderAlias } from "../secrets/ref-contract.js"; import { mergeTelegramAccountConfig, resolveDefaultTelegramAccountId, @@ -55,12 +60,58 @@ function inspectTokenFile(pathValue: unknown): { } } -function inspectTokenValue(value: unknown): { +function canResolveEnvSecretRefInReadOnlyPath(params: { + cfg: OpenClawConfig; + provider: string; + id: string; +}): boolean { + const providerConfig = params.cfg.secrets?.providers?.[params.provider]; + if (!providerConfig) { + return params.provider === resolveDefaultSecretProviderAlias(params.cfg, "env"); + } + if (providerConfig.source !== "env") { + return false; + } + const allowlist = providerConfig.allowlist; + return !allowlist || allowlist.includes(params.id); +} + +function inspectTokenValue(params: { cfg: OpenClawConfig; value: unknown }): { token: string; - tokenSource: "config" | "none"; + tokenSource: "config" | "env" | "none"; tokenStatus: TelegramCredentialStatus; } | null { - const token = normalizeSecretInputString(value); + // Try to resolve env-based SecretRefs from process.env for read-only inspection + const ref = coerceSecretRef(params.value, params.cfg.secrets?.defaults); + if (ref?.source === "env") { + if ( + !canResolveEnvSecretRefInReadOnlyPath({ + cfg: params.cfg, + provider: ref.provider, + id: ref.id, + }) + ) { + return { + token: "", + tokenSource: "env", + tokenStatus: "configured_unavailable", + }; + } + const envValue = process.env[ref.id]; + if (envValue && envValue.trim()) { + return { + token: envValue.trim(), + tokenSource: "env", + tokenStatus: "available", + }; + } + return { + token: "", + tokenSource: "env", + tokenStatus: "configured_unavailable", + }; + } + const token = normalizeSecretInputString(params.value); if (token) { return { token, @@ -68,7 +119,7 @@ function inspectTokenValue(value: unknown): { tokenStatus: "available", }; } - if (hasConfiguredSecretInput(value)) { + if (hasConfiguredSecretInput(params.value, params.cfg.secrets?.defaults)) { return { token: "", tokenSource: "config", @@ -102,7 +153,7 @@ function inspectTelegramAccountPrimary(params: { }; } - const accountToken = inspectTokenValue(accountConfig?.botToken); + const accountToken = inspectTokenValue({ cfg: params.cfg, value: accountConfig?.botToken }); if (accountToken) { return { accountId, @@ -130,7 +181,10 @@ function inspectTelegramAccountPrimary(params: { }; } - const channelToken = inspectTokenValue(params.cfg.channels?.telegram?.botToken); + const channelToken = inspectTokenValue({ + cfg: params.cfg, + value: params.cfg.channels?.telegram?.botToken, + }); if (channelToken) { return { accountId,