Skip to content

Commit e5bc278

Browse files
committed
fix(telegram): tolerate unresolved SecretRef in prompt-capability and reaction-guidance discovery
Codex round-1 review on PR #75445 found that even after hardening describeMessageTool, embedded prompt prep still crashes via two adjacent paths that go through resolveTelegramAccount BEFORE message-action discovery runs: 1. agentPrompt.messageToolCapabilities → resolveTelegramInlineButtonsScope 2. agentPrompt.reactionGuidance → resolveTelegramReactionLevel Both call resolveTelegramAccount on raw config, so a non-env SecretRef botToken still throws unresolved-SecretRef before model output. Wrap both with try/catch that returns the safe minimal default on unresolved-SecretRef and rethrows everything else, matching the pattern already used in extensions/{telegram,discord}/src/channel-actions.ts: - resolveTelegramInlineButtonsScope returns DEFAULT_INLINE_BUTTONS_SCOPE ("all") on unresolved SecretRef - resolveTelegramReactionLevel returns the resolveReactionLevel result with value=undefined → default "minimal" / fallback "ack" Add 4 new regression tests: - inline-buttons.test.ts: resolveTelegramInlineButtonsScope and isTelegramInlineButtonsEnabled both tolerate root-level and account-scoped SecretRef botTokens (do not throw) - reaction-level.test.ts: resolveTelegramReactionLevel returns the minimal-flag default for both root-level and account-scoped SecretRef botTokens (do not throw) Refs #75433
1 parent 8059393 commit e5bc278

4 files changed

Lines changed: 117 additions & 6 deletions

File tree

extensions/telegram/src/inline-buttons.test.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1+
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
12
import { describe, expect, it } from "vitest";
23
import { buildTelegramInteractiveButtons } from "./button-types.js";
34
import { describeTelegramInteractiveButtonBehavior } from "./button-types.test-helpers.js";
4-
import { resolveTelegramTargetChatType } from "./inline-buttons.js";
5+
import {
6+
isTelegramInlineButtonsEnabled,
7+
resolveTelegramInlineButtonsScope,
8+
resolveTelegramTargetChatType,
9+
} from "./inline-buttons.js";
510

611
describe("resolveTelegramTargetChatType", () => {
712
it("returns 'direct' for positive numeric IDs", () => {
@@ -85,3 +90,42 @@ describe("buildTelegramInteractiveButtons callback rewrites", () => {
8590
]);
8691
});
8792
});
93+
94+
describe("resolveTelegramInlineButtonsScope (#75433 SecretRef tolerance)", () => {
95+
// Embedded prompt prep calls this from raw config before the active runtime
96+
// snapshot has resolved channel credentials. Returning a benign default
97+
// instead of throwing keeps the embedded reply run alive.
98+
99+
it("returns the safe default scope when botToken is an unresolved SecretRef", () => {
100+
const cfg = {
101+
channels: {
102+
telegram: {
103+
botToken: { source: "exec", provider: "default", id: "telegram-token" },
104+
},
105+
},
106+
} as unknown as OpenClawConfig;
107+
108+
expect(() => resolveTelegramInlineButtonsScope({ cfg })).not.toThrow();
109+
// The DEFAULT_INLINE_BUTTONS_SCOPE is "all" — the contract is just that
110+
// we do not crash. Verify isTelegramInlineButtonsEnabled also tolerates
111+
// the same condition (used by describeMessageTool's buttonsEnabled flag).
112+
expect(() => isTelegramInlineButtonsEnabled({ cfg })).not.toThrow();
113+
});
114+
115+
it("returns the safe default scope when scoped account token is an unresolved SecretRef", () => {
116+
const cfg = {
117+
channels: {
118+
telegram: {
119+
accounts: {
120+
ops: {
121+
botToken: { source: "exec", provider: "default", id: "telegram-ops" },
122+
},
123+
},
124+
},
125+
},
126+
} as unknown as OpenClawConfig;
127+
128+
expect(() => resolveTelegramInlineButtonsScope({ cfg, accountId: "ops" })).not.toThrow();
129+
expect(() => isTelegramInlineButtonsEnabled({ cfg, accountId: "ops" })).not.toThrow();
130+
});
131+
});

extensions/telegram/src/inline-buttons.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,21 @@ export function resolveTelegramInlineButtonsScope(params: {
6060
cfg: OpenClawConfig;
6161
accountId?: string | null;
6262
}): TelegramInlineButtonsScope {
63-
const account = resolveTelegramAccount({ cfg: params.cfg, accountId: params.accountId });
63+
// Embedded prompt prep calls this from raw config before the active runtime
64+
// snapshot has resolved channel credentials. If channels.telegram.botToken is
65+
// a non-env SecretRef, `resolveTelegramAccount` throws an unresolved-SecretRef
66+
// error (#75433). Treat that as "no inline-button capability for prompt
67+
// discovery" instead of crashing the embedded reply run; the runtime send
68+
// path uses the resolved snapshot.
69+
let account: ReturnType<typeof resolveTelegramAccount>;
70+
try {
71+
account = resolveTelegramAccount({ cfg: params.cfg, accountId: params.accountId });
72+
} catch (err) {
73+
if (err instanceof Error && /unresolved SecretRef/i.test(err.message)) {
74+
return DEFAULT_INLINE_BUTTONS_SCOPE;
75+
}
76+
throw err;
77+
}
6478
return resolveTelegramInlineButtonsScopeFromCapabilities(account.config.capabilities);
6579
}
6680

extensions/telegram/src/reaction-level.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,4 +136,39 @@ describe("resolveTelegramReactionLevel", () => {
136136
const result = resolveTelegramReactionLevel({ cfg, accountId: "work" });
137137
expectMinimalFlags(result);
138138
});
139+
140+
// Regression for #75433: when channels.telegram.botToken is configured as
141+
// an unresolved SecretRef, prompt-prep reaction guidance must NOT throw and
142+
// crash the embedded reply run. Return the safe minimal default instead.
143+
it("falls back to minimal when botToken is an unresolved SecretRef instead of throwing", () => {
144+
const cfg = {
145+
channels: {
146+
telegram: {
147+
botToken: { source: "exec", provider: "default", id: "telegram-token" },
148+
},
149+
},
150+
} as unknown as OpenClawConfig;
151+
152+
expect(() => resolveTelegramReactionLevel({ cfg })).not.toThrow();
153+
const result = resolveTelegramReactionLevel({ cfg });
154+
expectMinimalFlags(result);
155+
});
156+
157+
it("falls back to minimal when scoped account token is an unresolved SecretRef instead of throwing", () => {
158+
const cfg = {
159+
channels: {
160+
telegram: {
161+
accounts: {
162+
ops: {
163+
botToken: { source: "exec", provider: "default", id: "telegram-ops" },
164+
},
165+
},
166+
},
167+
},
168+
} as unknown as OpenClawConfig;
169+
170+
expect(() => resolveTelegramReactionLevel({ cfg, accountId: "ops" })).not.toThrow();
171+
const result = resolveTelegramReactionLevel({ cfg, accountId: "ops" });
172+
expectMinimalFlags(result);
173+
});
139174
});

extensions/telegram/src/reaction-level.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,28 @@ export function resolveTelegramReactionLevel(params: {
1616
cfg: OpenClawConfig;
1717
accountId?: string;
1818
}): ResolvedReactionLevel {
19-
const account = resolveTelegramAccount({
20-
cfg: params.cfg,
21-
accountId: params.accountId,
22-
});
19+
// Prompt prep calls this from raw config before the active runtime snapshot
20+
// has resolved channel credentials. When channels.telegram.botToken is a
21+
// non-env SecretRef, `resolveTelegramAccount` throws an unresolved-SecretRef
22+
// error (#75433); treat that as the safe minimal default for discovery
23+
// instead of crashing the embedded reply run. The runtime delivery path
24+
// uses the resolved snapshot anyway.
25+
let account: ReturnType<typeof resolveTelegramAccount>;
26+
try {
27+
account = resolveTelegramAccount({
28+
cfg: params.cfg,
29+
accountId: params.accountId,
30+
});
31+
} catch (err) {
32+
if (err instanceof Error && /unresolved SecretRef/i.test(err.message)) {
33+
return resolveReactionLevel({
34+
value: undefined,
35+
defaultLevel: "minimal",
36+
invalidFallback: "ack",
37+
});
38+
}
39+
throw err;
40+
}
2341
return resolveReactionLevel({
2442
value: account.config.reactionLevel,
2543
defaultLevel: "minimal",

0 commit comments

Comments
 (0)