Skip to content

Commit 90f3007

Browse files
authored
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>
1 parent ee57f34 commit 90f3007

9 files changed

Lines changed: 340 additions & 42 deletions

extensions/telegram/src/account-config.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {
22
normalizeAccountId,
3-
resolveAccountEntry,
3+
resolveNormalizedAccountEntry,
44
type OpenClawConfig,
55
} from "openclaw/plugin-sdk/account-core";
66
import type { TelegramAccountConfig } from "openclaw/plugin-sdk/config-contracts";
@@ -49,7 +49,11 @@ export function resolveTelegramAccountConfig(
4949
accountId: string,
5050
): TelegramAccountConfig | undefined {
5151
const normalized = normalizeAccountId(accountId);
52-
return resolveAccountEntry(cfg.channels?.telegram?.accounts, normalized);
52+
return resolveNormalizedAccountEntry(
53+
cfg.channels?.telegram?.accounts,
54+
normalized,
55+
normalizeAccountId,
56+
);
5357
}
5458

5559
export function mergeTelegramAccountConfig(

extensions/telegram/src/account-inspect.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,52 @@ describe("inspectTelegramAccount SecretRef resolution", () => {
8080
});
8181
});
8282

83+
it("matches runtime token lookup for account keys that need full normalization", () => {
84+
const cfg: OpenClawConfig = {
85+
channels: {
86+
telegram: {
87+
accounts: {
88+
"Carey Notifications": {
89+
botToken: "123:token",
90+
reactionLevel: "ack",
91+
},
92+
},
93+
},
94+
},
95+
};
96+
97+
const account = inspectTelegramAccount({
98+
cfg,
99+
accountId: "carey-notifications",
100+
});
101+
102+
expect(account.accountId).toBe("carey-notifications");
103+
expect(account.configured).toBe(true);
104+
expect(account.tokenSource).toBe("config");
105+
expect(account.tokenStatus).toBe("available");
106+
expect(account.config.reactionLevel).toBe("ack");
107+
});
108+
109+
it("blocks channel-token fallback for unknown scoped accounts in multi-account config", () => {
110+
const cfg: OpenClawConfig = {
111+
channels: {
112+
telegram: {
113+
botToken: "123:channel",
114+
accounts: {
115+
work: { botToken: "123:work" },
116+
},
117+
},
118+
},
119+
};
120+
121+
const account = inspectTelegramAccount({ cfg, accountId: "unknown" });
122+
123+
expect(account.accountId).toBe("unknown");
124+
expect(account.configured).toBe(false);
125+
expect(account.tokenSource).toBe("none");
126+
expect(account.tokenStatus).toBe("missing");
127+
});
128+
83129
it.runIf(process.platform !== "win32")(
84130
"treats symlinked token files as configured_unavailable",
85131
() => {

extensions/telegram/src/account-inspect.ts

Lines changed: 44 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,16 @@ function inspectTokenValue(params: { cfg: OpenClawConfig; value: unknown }): {
130130
return null;
131131
}
132132

133+
function hasConfiguredTelegramAccounts(cfg: OpenClawConfig): boolean {
134+
const accounts = cfg.channels?.telegram?.accounts;
135+
return (
136+
!!accounts &&
137+
typeof accounts === "object" &&
138+
!Array.isArray(accounts) &&
139+
Object.keys(accounts).length > 0
140+
);
141+
}
142+
133143
function inspectTelegramAccountPrimary(params: {
134144
cfg: OpenClawConfig;
135145
accountId: string;
@@ -140,6 +150,10 @@ function inspectTelegramAccountPrimary(params: {
140150
const enabled = params.cfg.channels?.telegram?.enabled !== false && merged.enabled !== false;
141151

142152
const accountConfig = resolveTelegramAccountConfig(params.cfg, accountId);
153+
const allowChannelCredentialFallback =
154+
accountId === DEFAULT_ACCOUNT_ID ||
155+
!!accountConfig ||
156+
!hasConfiguredTelegramAccounts(params.cfg);
143157
const accountTokenFile = inspectTokenFile(accountConfig?.tokenFile);
144158
if (accountTokenFile) {
145159
return {
@@ -168,35 +182,37 @@ function inspectTelegramAccountPrimary(params: {
168182
};
169183
}
170184

171-
const channelTokenFile = inspectTokenFile(params.cfg.channels?.telegram?.tokenFile);
172-
if (channelTokenFile) {
173-
return {
174-
accountId,
175-
enabled,
176-
name: normalizeOptionalString(merged.name),
177-
token: channelTokenFile.token,
178-
tokenSource: channelTokenFile.tokenSource,
179-
tokenStatus: channelTokenFile.tokenStatus,
180-
configured: channelTokenFile.tokenStatus !== "missing",
181-
config: merged,
182-
};
183-
}
185+
if (allowChannelCredentialFallback) {
186+
const channelTokenFile = inspectTokenFile(params.cfg.channels?.telegram?.tokenFile);
187+
if (channelTokenFile) {
188+
return {
189+
accountId,
190+
enabled,
191+
name: normalizeOptionalString(merged.name),
192+
token: channelTokenFile.token,
193+
tokenSource: channelTokenFile.tokenSource,
194+
tokenStatus: channelTokenFile.tokenStatus,
195+
configured: channelTokenFile.tokenStatus !== "missing",
196+
config: merged,
197+
};
198+
}
184199

185-
const channelToken = inspectTokenValue({
186-
cfg: params.cfg,
187-
value: params.cfg.channels?.telegram?.botToken,
188-
});
189-
if (channelToken) {
190-
return {
191-
accountId,
192-
enabled,
193-
name: normalizeOptionalString(merged.name),
194-
token: channelToken.token,
195-
tokenSource: channelToken.tokenSource,
196-
tokenStatus: channelToken.tokenStatus,
197-
configured: channelToken.tokenStatus !== "missing",
198-
config: merged,
199-
};
200+
const channelToken = inspectTokenValue({
201+
cfg: params.cfg,
202+
value: params.cfg.channels?.telegram?.botToken,
203+
});
204+
if (channelToken) {
205+
return {
206+
accountId,
207+
enabled,
208+
name: normalizeOptionalString(merged.name),
209+
token: channelToken.token,
210+
tokenSource: channelToken.tokenSource,
211+
tokenStatus: channelToken.tokenStatus,
212+
configured: channelToken.tokenStatus !== "missing",
213+
config: merged,
214+
};
215+
}
200216
}
201217

202218
const allowEnv = accountId === DEFAULT_ACCOUNT_ID;

extensions/telegram/src/channel-actions.test.ts

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,4 +316,132 @@ describe("telegramMessageActions", () => {
316316
}
317317
}
318318
});
319+
320+
// Regression for #75433: prompt discovery reads raw config before the active
321+
// runtime snapshot has resolved SecretRefs. Treat SecretRef-backed accounts
322+
// as configured and keep advertising config-derived actions.
323+
it("describes discovery when botToken is an unresolved SecretRef instead of crashing the embedded run", () => {
324+
const cfg = {
325+
channels: {
326+
telegram: {
327+
botToken: { source: "exec", provider: "default", id: "telegram-token" },
328+
actions: {
329+
reactions: true,
330+
poll: false,
331+
},
332+
},
333+
},
334+
} as unknown as OpenClawConfig;
335+
336+
const discovery = telegramMessageActions.describeMessageTool?.({ cfg });
337+
338+
expect(discovery?.actions).toContain("send");
339+
expect(discovery?.actions).toContain("react");
340+
expect(discovery?.actions).not.toContain("poll");
341+
});
342+
343+
it("describes scoped account discovery when Telegram account token is an unresolved SecretRef", () => {
344+
const cfg = {
345+
channels: {
346+
telegram: {
347+
accounts: {
348+
ops: {
349+
botToken: { source: "exec", provider: "default", id: "telegram-ops" },
350+
actions: {
351+
reactions: false,
352+
poll: true,
353+
},
354+
},
355+
},
356+
},
357+
},
358+
} as unknown as OpenClawConfig;
359+
360+
const discovery = telegramMessageActions.describeMessageTool?.({
361+
cfg,
362+
accountId: "ops",
363+
});
364+
365+
expect(discovery?.actions).toContain("send");
366+
expect(discovery?.actions).toContain("poll");
367+
expect(discovery?.actions).not.toContain("react");
368+
});
369+
370+
it("matches runtime account-key normalization during SecretRef-tolerant discovery", () => {
371+
const cfg = {
372+
channels: {
373+
telegram: {
374+
accounts: {
375+
"Carey Notifications": {
376+
botToken: { source: "exec", provider: "default", id: "telegram-carey" },
377+
actions: {
378+
poll: true,
379+
reactions: false,
380+
},
381+
},
382+
},
383+
},
384+
},
385+
} as unknown as OpenClawConfig;
386+
387+
const discovery = telegramMessageActions.describeMessageTool?.({
388+
cfg,
389+
accountId: "carey-notifications",
390+
});
391+
392+
expect(discovery?.actions).toContain("send");
393+
expect(discovery?.actions).toContain("poll");
394+
expect(discovery?.actions).not.toContain("react");
395+
});
396+
397+
it("does not discover unknown scoped accounts via channel-level fallback in multi-account config", () => {
398+
const cfg = {
399+
channels: {
400+
telegram: {
401+
botToken: "tok-channel",
402+
accounts: {
403+
work: { botToken: "tok-work" },
404+
},
405+
},
406+
},
407+
} as OpenClawConfig;
408+
409+
expect(
410+
telegramMessageActions.describeMessageTool?.({
411+
cfg,
412+
accountId: "unknown",
413+
})?.actions,
414+
).toEqual([]);
415+
});
416+
417+
it("keeps healthy Telegram accounts discoverable when a sibling token is an unresolved SecretRef", () => {
418+
const cfg = {
419+
channels: {
420+
telegram: {
421+
accounts: {
422+
unresolved: {
423+
botToken: { source: "exec", provider: "default", id: "telegram-unresolved" },
424+
actions: {
425+
reactions: false,
426+
poll: false,
427+
},
428+
},
429+
healthy: {
430+
botToken: "tok-healthy",
431+
actions: {
432+
reactions: true,
433+
poll: false,
434+
},
435+
},
436+
},
437+
},
438+
},
439+
} as unknown as OpenClawConfig;
440+
441+
const discovery = telegramMessageActions.describeMessageTool?.({ cfg });
442+
443+
expect(discovery?.actions).toContain("send");
444+
expect(discovery?.actions).toContain("react");
445+
expect(discovery?.actions).not.toContain("poll");
446+
});
319447
});

extensions/telegram/src/channel-actions.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ import type {
1212
import type { TelegramActionConfig } from "openclaw/plugin-sdk/config-contracts";
1313
import { readStringValue } from "openclaw/plugin-sdk/string-coerce-runtime";
1414
import { extractToolSend } from "openclaw/plugin-sdk/tool-send";
15+
import { inspectTelegramAccount } from "./account-inspect.js";
1516
import {
1617
createTelegramActionGate,
17-
listEnabledTelegramAccounts,
18-
resolveTelegramAccount,
18+
listTelegramAccountIds,
1919
resolveTelegramPollActionGateState,
2020
} from "./accounts.js";
2121
import { isTelegramInlineButtonsEnabled } from "./inline-buttons.js";
@@ -53,8 +53,11 @@ function resolveTelegramMessageActionName(action: ChannelMessageActionName) {
5353
return TELEGRAM_MESSAGE_ACTION_MAP[action as keyof typeof TELEGRAM_MESSAGE_ACTION_MAP];
5454
}
5555

56-
function resolveTelegramActionDiscovery(cfg: Parameters<typeof listEnabledTelegramAccounts>[0]) {
57-
const accounts = listTokenSourcedAccounts(listEnabledTelegramAccounts(cfg));
56+
function resolveTelegramActionDiscovery(cfg: Parameters<typeof listTelegramAccountIds>[0]) {
57+
const inspected = listTelegramAccountIds(cfg)
58+
.map((accountId) => inspectTelegramAccount({ cfg, accountId }))
59+
.filter((account) => account.enabled && account.configured);
60+
const accounts = listTokenSourcedAccounts(inspected);
5861
if (accounts.length === 0) {
5962
return null;
6063
}
@@ -83,14 +86,14 @@ function resolveTelegramActionDiscovery(cfg: Parameters<typeof listEnabledTelegr
8386
}
8487

8588
function resolveScopedTelegramActionDiscovery(params: {
86-
cfg: Parameters<typeof listEnabledTelegramAccounts>[0];
89+
cfg: Parameters<typeof listTelegramAccountIds>[0];
8790
accountId?: string | null;
8891
}) {
8992
if (!params.accountId) {
9093
return resolveTelegramActionDiscovery(params.cfg);
9194
}
92-
const account = resolveTelegramAccount({ cfg: params.cfg, accountId: params.accountId });
93-
if (!account.enabled || account.tokenSource === "none") {
95+
const account = inspectTelegramAccount({ cfg: params.cfg, accountId: params.accountId });
96+
if (!account.enabled || !account.configured || account.tokenSource === "none") {
9497
return null;
9598
}
9699
const gate = createTelegramActionGate({

0 commit comments

Comments
 (0)