mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
fix(channels): preserve Telegram SecretRef prompt config
Use read-only Telegram account inspection for prompt-time channel actions, inline buttons, and reaction guidance so unresolved SecretRef tokens retain configured non-secret behavior before runtime snapshot hydration. Match runtime Telegram account lookup for normalized config keys and multi-account fallback guards, while keeping sends/actions on the existing strict credential resolution path. Fixes #75433. Co-authored-by: Shubhankar Tripathy <reach2shubhankar@gmail.com>
This commit is contained in:
committed by
GitHub
parent
ee57f341f0
commit
90f30075aa
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
normalizeAccountId,
|
||||
resolveAccountEntry,
|
||||
resolveNormalizedAccountEntry,
|
||||
type OpenClawConfig,
|
||||
} from "openclaw/plugin-sdk/account-core";
|
||||
import type { TelegramAccountConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
@@ -49,7 +49,11 @@ export function resolveTelegramAccountConfig(
|
||||
accountId: string,
|
||||
): TelegramAccountConfig | undefined {
|
||||
const normalized = normalizeAccountId(accountId);
|
||||
return resolveAccountEntry(cfg.channels?.telegram?.accounts, normalized);
|
||||
return resolveNormalizedAccountEntry(
|
||||
cfg.channels?.telegram?.accounts,
|
||||
normalized,
|
||||
normalizeAccountId,
|
||||
);
|
||||
}
|
||||
|
||||
export function mergeTelegramAccountConfig(
|
||||
|
||||
@@ -80,6 +80,52 @@ describe("inspectTelegramAccount SecretRef resolution", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("matches runtime token lookup for account keys that need full normalization", () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
channels: {
|
||||
telegram: {
|
||||
accounts: {
|
||||
"Carey Notifications": {
|
||||
botToken: "123:token",
|
||||
reactionLevel: "ack",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const account = inspectTelegramAccount({
|
||||
cfg,
|
||||
accountId: "carey-notifications",
|
||||
});
|
||||
|
||||
expect(account.accountId).toBe("carey-notifications");
|
||||
expect(account.configured).toBe(true);
|
||||
expect(account.tokenSource).toBe("config");
|
||||
expect(account.tokenStatus).toBe("available");
|
||||
expect(account.config.reactionLevel).toBe("ack");
|
||||
});
|
||||
|
||||
it("blocks channel-token fallback for unknown scoped accounts in multi-account config", () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
channels: {
|
||||
telegram: {
|
||||
botToken: "123:channel",
|
||||
accounts: {
|
||||
work: { botToken: "123:work" },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const account = inspectTelegramAccount({ cfg, accountId: "unknown" });
|
||||
|
||||
expect(account.accountId).toBe("unknown");
|
||||
expect(account.configured).toBe(false);
|
||||
expect(account.tokenSource).toBe("none");
|
||||
expect(account.tokenStatus).toBe("missing");
|
||||
});
|
||||
|
||||
it.runIf(process.platform !== "win32")(
|
||||
"treats symlinked token files as configured_unavailable",
|
||||
() => {
|
||||
|
||||
@@ -130,6 +130,16 @@ function inspectTokenValue(params: { cfg: OpenClawConfig; value: unknown }): {
|
||||
return null;
|
||||
}
|
||||
|
||||
function hasConfiguredTelegramAccounts(cfg: OpenClawConfig): boolean {
|
||||
const accounts = cfg.channels?.telegram?.accounts;
|
||||
return (
|
||||
!!accounts &&
|
||||
typeof accounts === "object" &&
|
||||
!Array.isArray(accounts) &&
|
||||
Object.keys(accounts).length > 0
|
||||
);
|
||||
}
|
||||
|
||||
function inspectTelegramAccountPrimary(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId: string;
|
||||
@@ -140,6 +150,10 @@ function inspectTelegramAccountPrimary(params: {
|
||||
const enabled = params.cfg.channels?.telegram?.enabled !== false && merged.enabled !== false;
|
||||
|
||||
const accountConfig = resolveTelegramAccountConfig(params.cfg, accountId);
|
||||
const allowChannelCredentialFallback =
|
||||
accountId === DEFAULT_ACCOUNT_ID ||
|
||||
!!accountConfig ||
|
||||
!hasConfiguredTelegramAccounts(params.cfg);
|
||||
const accountTokenFile = inspectTokenFile(accountConfig?.tokenFile);
|
||||
if (accountTokenFile) {
|
||||
return {
|
||||
@@ -168,35 +182,37 @@ function inspectTelegramAccountPrimary(params: {
|
||||
};
|
||||
}
|
||||
|
||||
const channelTokenFile = inspectTokenFile(params.cfg.channels?.telegram?.tokenFile);
|
||||
if (channelTokenFile) {
|
||||
return {
|
||||
accountId,
|
||||
enabled,
|
||||
name: normalizeOptionalString(merged.name),
|
||||
token: channelTokenFile.token,
|
||||
tokenSource: channelTokenFile.tokenSource,
|
||||
tokenStatus: channelTokenFile.tokenStatus,
|
||||
configured: channelTokenFile.tokenStatus !== "missing",
|
||||
config: merged,
|
||||
};
|
||||
}
|
||||
if (allowChannelCredentialFallback) {
|
||||
const channelTokenFile = inspectTokenFile(params.cfg.channels?.telegram?.tokenFile);
|
||||
if (channelTokenFile) {
|
||||
return {
|
||||
accountId,
|
||||
enabled,
|
||||
name: normalizeOptionalString(merged.name),
|
||||
token: channelTokenFile.token,
|
||||
tokenSource: channelTokenFile.tokenSource,
|
||||
tokenStatus: channelTokenFile.tokenStatus,
|
||||
configured: channelTokenFile.tokenStatus !== "missing",
|
||||
config: merged,
|
||||
};
|
||||
}
|
||||
|
||||
const channelToken = inspectTokenValue({
|
||||
cfg: params.cfg,
|
||||
value: params.cfg.channels?.telegram?.botToken,
|
||||
});
|
||||
if (channelToken) {
|
||||
return {
|
||||
accountId,
|
||||
enabled,
|
||||
name: normalizeOptionalString(merged.name),
|
||||
token: channelToken.token,
|
||||
tokenSource: channelToken.tokenSource,
|
||||
tokenStatus: channelToken.tokenStatus,
|
||||
configured: channelToken.tokenStatus !== "missing",
|
||||
config: merged,
|
||||
};
|
||||
const channelToken = inspectTokenValue({
|
||||
cfg: params.cfg,
|
||||
value: params.cfg.channels?.telegram?.botToken,
|
||||
});
|
||||
if (channelToken) {
|
||||
return {
|
||||
accountId,
|
||||
enabled,
|
||||
name: normalizeOptionalString(merged.name),
|
||||
token: channelToken.token,
|
||||
tokenSource: channelToken.tokenSource,
|
||||
tokenStatus: channelToken.tokenStatus,
|
||||
configured: channelToken.tokenStatus !== "missing",
|
||||
config: merged,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const allowEnv = accountId === DEFAULT_ACCOUNT_ID;
|
||||
|
||||
@@ -316,4 +316,132 @@ describe("telegramMessageActions", () => {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Regression for #75433: prompt discovery reads raw config before the active
|
||||
// runtime snapshot has resolved SecretRefs. Treat SecretRef-backed accounts
|
||||
// as configured and keep advertising config-derived actions.
|
||||
it("describes discovery when botToken is an unresolved SecretRef instead of crashing the embedded run", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
telegram: {
|
||||
botToken: { source: "exec", provider: "default", id: "telegram-token" },
|
||||
actions: {
|
||||
reactions: true,
|
||||
poll: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
|
||||
const discovery = telegramMessageActions.describeMessageTool?.({ cfg });
|
||||
|
||||
expect(discovery?.actions).toContain("send");
|
||||
expect(discovery?.actions).toContain("react");
|
||||
expect(discovery?.actions).not.toContain("poll");
|
||||
});
|
||||
|
||||
it("describes scoped account discovery when Telegram account token is an unresolved SecretRef", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
telegram: {
|
||||
accounts: {
|
||||
ops: {
|
||||
botToken: { source: "exec", provider: "default", id: "telegram-ops" },
|
||||
actions: {
|
||||
reactions: false,
|
||||
poll: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
|
||||
const discovery = telegramMessageActions.describeMessageTool?.({
|
||||
cfg,
|
||||
accountId: "ops",
|
||||
});
|
||||
|
||||
expect(discovery?.actions).toContain("send");
|
||||
expect(discovery?.actions).toContain("poll");
|
||||
expect(discovery?.actions).not.toContain("react");
|
||||
});
|
||||
|
||||
it("matches runtime account-key normalization during SecretRef-tolerant discovery", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
telegram: {
|
||||
accounts: {
|
||||
"Carey Notifications": {
|
||||
botToken: { source: "exec", provider: "default", id: "telegram-carey" },
|
||||
actions: {
|
||||
poll: true,
|
||||
reactions: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
|
||||
const discovery = telegramMessageActions.describeMessageTool?.({
|
||||
cfg,
|
||||
accountId: "carey-notifications",
|
||||
});
|
||||
|
||||
expect(discovery?.actions).toContain("send");
|
||||
expect(discovery?.actions).toContain("poll");
|
||||
expect(discovery?.actions).not.toContain("react");
|
||||
});
|
||||
|
||||
it("does not discover unknown scoped accounts via channel-level fallback in multi-account config", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
telegram: {
|
||||
botToken: "tok-channel",
|
||||
accounts: {
|
||||
work: { botToken: "tok-work" },
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
expect(
|
||||
telegramMessageActions.describeMessageTool?.({
|
||||
cfg,
|
||||
accountId: "unknown",
|
||||
})?.actions,
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps healthy Telegram accounts discoverable when a sibling token is an unresolved SecretRef", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
telegram: {
|
||||
accounts: {
|
||||
unresolved: {
|
||||
botToken: { source: "exec", provider: "default", id: "telegram-unresolved" },
|
||||
actions: {
|
||||
reactions: false,
|
||||
poll: false,
|
||||
},
|
||||
},
|
||||
healthy: {
|
||||
botToken: "tok-healthy",
|
||||
actions: {
|
||||
reactions: true,
|
||||
poll: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
|
||||
const discovery = telegramMessageActions.describeMessageTool?.({ cfg });
|
||||
|
||||
expect(discovery?.actions).toContain("send");
|
||||
expect(discovery?.actions).toContain("react");
|
||||
expect(discovery?.actions).not.toContain("poll");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,10 +12,10 @@ import type {
|
||||
import type { TelegramActionConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { readStringValue } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { extractToolSend } from "openclaw/plugin-sdk/tool-send";
|
||||
import { inspectTelegramAccount } from "./account-inspect.js";
|
||||
import {
|
||||
createTelegramActionGate,
|
||||
listEnabledTelegramAccounts,
|
||||
resolveTelegramAccount,
|
||||
listTelegramAccountIds,
|
||||
resolveTelegramPollActionGateState,
|
||||
} from "./accounts.js";
|
||||
import { isTelegramInlineButtonsEnabled } from "./inline-buttons.js";
|
||||
@@ -53,8 +53,11 @@ function resolveTelegramMessageActionName(action: ChannelMessageActionName) {
|
||||
return TELEGRAM_MESSAGE_ACTION_MAP[action as keyof typeof TELEGRAM_MESSAGE_ACTION_MAP];
|
||||
}
|
||||
|
||||
function resolveTelegramActionDiscovery(cfg: Parameters<typeof listEnabledTelegramAccounts>[0]) {
|
||||
const accounts = listTokenSourcedAccounts(listEnabledTelegramAccounts(cfg));
|
||||
function resolveTelegramActionDiscovery(cfg: Parameters<typeof listTelegramAccountIds>[0]) {
|
||||
const inspected = listTelegramAccountIds(cfg)
|
||||
.map((accountId) => inspectTelegramAccount({ cfg, accountId }))
|
||||
.filter((account) => account.enabled && account.configured);
|
||||
const accounts = listTokenSourcedAccounts(inspected);
|
||||
if (accounts.length === 0) {
|
||||
return null;
|
||||
}
|
||||
@@ -83,14 +86,14 @@ function resolveTelegramActionDiscovery(cfg: Parameters<typeof listEnabledTelegr
|
||||
}
|
||||
|
||||
function resolveScopedTelegramActionDiscovery(params: {
|
||||
cfg: Parameters<typeof listEnabledTelegramAccounts>[0];
|
||||
cfg: Parameters<typeof listTelegramAccountIds>[0];
|
||||
accountId?: string | null;
|
||||
}) {
|
||||
if (!params.accountId) {
|
||||
return resolveTelegramActionDiscovery(params.cfg);
|
||||
}
|
||||
const account = resolveTelegramAccount({ cfg: params.cfg, accountId: params.accountId });
|
||||
if (!account.enabled || account.tokenSource === "none") {
|
||||
const account = inspectTelegramAccount({ cfg: params.cfg, accountId: params.accountId });
|
||||
if (!account.enabled || !account.configured || account.tokenSource === "none") {
|
||||
return null;
|
||||
}
|
||||
const gate = createTelegramActionGate({
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildTelegramInteractiveButtons } from "./button-types.js";
|
||||
import { describeTelegramInteractiveButtonBehavior } from "./button-types.test-helpers.js";
|
||||
import { resolveTelegramTargetChatType } from "./inline-buttons.js";
|
||||
import {
|
||||
isTelegramInlineButtonsEnabled,
|
||||
resolveTelegramInlineButtonsScope,
|
||||
resolveTelegramTargetChatType,
|
||||
} from "./inline-buttons.js";
|
||||
|
||||
describe("resolveTelegramTargetChatType", () => {
|
||||
it("returns 'direct' for positive numeric IDs", () => {
|
||||
@@ -85,3 +90,53 @@ describe("buildTelegramInteractiveButtons callback rewrites", () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveTelegramInlineButtonsScope (#75433 SecretRef tolerance)", () => {
|
||||
// Embedded prompt prep calls this from raw config before the active runtime
|
||||
// snapshot has resolved channel credentials. Read-only account inspection
|
||||
// keeps SecretRef-backed config readable without resolving the token.
|
||||
it("preserves the default inline-buttons scope when botToken is an unresolved SecretRef", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
telegram: {
|
||||
botToken: { source: "exec", provider: "default", id: "telegram-token" },
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
|
||||
expect(resolveTelegramInlineButtonsScope({ cfg })).toBe("allowlist");
|
||||
expect(isTelegramInlineButtonsEnabled({ cfg })).toBe(true);
|
||||
});
|
||||
|
||||
it('preserves configured "off" when botToken is an unresolved SecretRef', () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
telegram: {
|
||||
botToken: { source: "exec", provider: "default", id: "telegram-token" },
|
||||
capabilities: { inlineButtons: "off" },
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
|
||||
expect(resolveTelegramInlineButtonsScope({ cfg })).toBe("off");
|
||||
expect(isTelegramInlineButtonsEnabled({ cfg })).toBe(false);
|
||||
});
|
||||
|
||||
it("preserves scoped account inline-buttons config when the token is an unresolved SecretRef", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
telegram: {
|
||||
accounts: {
|
||||
ops: {
|
||||
botToken: { source: "exec", provider: "default", id: "telegram-ops" },
|
||||
capabilities: { inlineButtons: "all" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
|
||||
expect(resolveTelegramInlineButtonsScope({ cfg, accountId: "ops" })).toBe("all");
|
||||
expect(isTelegramInlineButtonsEnabled({ cfg, accountId: "ops" })).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,8 @@ import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
normalizeOptionalLowercaseString,
|
||||
} from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { listTelegramAccountIds, resolveTelegramAccount } from "./accounts.js";
|
||||
import { inspectTelegramAccount } from "./account-inspect.js";
|
||||
import { listTelegramAccountIds } from "./accounts.js";
|
||||
|
||||
const DEFAULT_INLINE_BUTTONS_SCOPE: TelegramInlineButtonsScope = "allowlist";
|
||||
|
||||
@@ -60,7 +61,7 @@ export function resolveTelegramInlineButtonsScope(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
}): TelegramInlineButtonsScope {
|
||||
const account = resolveTelegramAccount({ cfg: params.cfg, accountId: params.accountId });
|
||||
const account = inspectTelegramAccount({ cfg: params.cfg, accountId: params.accountId });
|
||||
return resolveTelegramInlineButtonsScopeFromCapabilities(account.config.capabilities);
|
||||
}
|
||||
|
||||
|
||||
@@ -136,4 +136,49 @@ describe("resolveTelegramReactionLevel", () => {
|
||||
const result = resolveTelegramReactionLevel({ cfg, accountId: "work" });
|
||||
expectMinimalFlags(result);
|
||||
});
|
||||
|
||||
// Regression for #75433: prompt-prep reaction guidance reads raw config
|
||||
// before the active runtime snapshot has resolved channel credentials.
|
||||
it("preserves configured reactionLevel when botToken is an unresolved SecretRef", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
telegram: {
|
||||
botToken: { source: "exec", provider: "default", id: "telegram-token" },
|
||||
reactionLevel: "off",
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
|
||||
expect(() => resolveTelegramReactionLevel({ cfg })).not.toThrow();
|
||||
const result = resolveTelegramReactionLevel({ cfg });
|
||||
expectReactionFlags(result, {
|
||||
level: "off",
|
||||
ackEnabled: false,
|
||||
agentReactionsEnabled: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves scoped account reactionLevel when token is an unresolved SecretRef", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
telegram: {
|
||||
reactionLevel: "minimal",
|
||||
accounts: {
|
||||
ops: {
|
||||
botToken: { source: "exec", provider: "default", id: "telegram-ops" },
|
||||
reactionLevel: "ack",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
|
||||
expect(() => resolveTelegramReactionLevel({ cfg, accountId: "ops" })).not.toThrow();
|
||||
const result = resolveTelegramReactionLevel({ cfg, accountId: "ops" });
|
||||
expectReactionFlags(result, {
|
||||
level: "ack",
|
||||
ackEnabled: true,
|
||||
agentReactionsEnabled: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
type ReactionLevel,
|
||||
type ResolvedReactionLevel as BaseResolvedReactionLevel,
|
||||
} from "openclaw/plugin-sdk/status-helpers";
|
||||
import { resolveTelegramAccount } from "./accounts.js";
|
||||
import { inspectTelegramAccount } from "./account-inspect.js";
|
||||
|
||||
export type TelegramReactionLevel = ReactionLevel;
|
||||
export type ResolvedReactionLevel = BaseResolvedReactionLevel;
|
||||
@@ -16,7 +16,7 @@ export function resolveTelegramReactionLevel(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string;
|
||||
}): ResolvedReactionLevel {
|
||||
const account = resolveTelegramAccount({
|
||||
const account = inspectTelegramAccount({
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user