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>
This commit is contained in:
Peter Steinberger
2026-03-07 20:50:07 +00:00
parent 2015ab3194
commit 330579ef96
3 changed files with 141 additions and 7 deletions

View File

@@ -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. - 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. - 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/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 ## 2026.3.2

View File

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

View File

@@ -1,9 +1,14 @@
import fs from "node:fs"; import fs from "node:fs";
import type { OpenClawConfig } from "../config/config.js"; 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 type { TelegramAccountConfig } from "../config/types.telegram.js";
import { resolveAccountWithDefaultFallback } from "../plugin-sdk/account-resolution.js"; import { resolveAccountWithDefaultFallback } from "../plugin-sdk/account-resolution.js";
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js"; import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js";
import { resolveDefaultSecretProviderAlias } from "../secrets/ref-contract.js";
import { import {
mergeTelegramAccountConfig, mergeTelegramAccountConfig,
resolveDefaultTelegramAccountId, 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; token: string;
tokenSource: "config" | "none"; tokenSource: "config" | "env" | "none";
tokenStatus: TelegramCredentialStatus; tokenStatus: TelegramCredentialStatus;
} | null { } | 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) { if (token) {
return { return {
token, token,
@@ -68,7 +119,7 @@ function inspectTokenValue(value: unknown): {
tokenStatus: "available", tokenStatus: "available",
}; };
} }
if (hasConfiguredSecretInput(value)) { if (hasConfiguredSecretInput(params.value, params.cfg.secrets?.defaults)) {
return { return {
token: "", token: "",
tokenSource: "config", 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) { if (accountToken) {
return { return {
accountId, 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) { if (channelToken) {
return { return {
accountId, accountId,