M2: Add proactive outbound Telegram messaging primitive#6222
Conversation
Co-Authored-By: Claude <noreply@anthropic.com>
| export const telegramBotMessagingProvider: MessagingProvider = { | ||
| id: 'telegram', | ||
| displayName: 'Telegram', | ||
| credentialService: 'telegram', |
There was a problem hiding this comment.
🔴 Telegram provider unusable via messaging tools: withProviderToken throws because no access_token exists
All messaging skill tools (send, reply, auth-test, list, search, read, etc.) call withProviderToken(provider, fn) which delegates to withValidToken(provider.credentialService, fn). For the Telegram provider, credentialService is 'telegram', so withValidToken looks for credential:telegram:access_token at assistant/src/security/token-manager.ts:110. However, Telegram stores its credential as credential:telegram:bot_token (see adapter.ts:47). Since no access_token key exists, withValidToken throws a TokenExpiredError at line 116 before the callback is ever invoked.
Root Cause and Impact
The flow is:
messaging-send.ts:20callswithProviderToken(provider, async (token) => { ... })shared.ts:44-45callswithValidToken(provider.credentialService, fn)→withValidToken('telegram', fn)token-manager.ts:110doesgetSecureKey('credential:telegram:access_token')→ returnsundefinedtoken-manager.ts:116throwsTokenExpiredError('No access token found for "telegram"')
The Telegram adapter's sendMessage (which correctly uses the gateway bearer token, not an OAuth token) is never reached. This makes the Telegram provider completely non-functional through the standard messaging tool interface, despite being registered and appearing as "connected" via isConnected().
Impact: Every attempt to send a Telegram message via the messaging tools will fail with a misleading "No access token found" error, even when the bot token is properly configured.
Prompt for agents
The core issue is that all messaging tools use withProviderToken() which calls withValidToken(service, fn), and withValidToken looks for credential:{service}:access_token. Since Telegram uses bot_token instead of access_token, this always throws.
There are two approaches to fix this:
1. (Recommended) Update assistant/src/config/bundled-skills/messaging/tools/shared.ts to add a Telegram-aware path in withProviderToken. For example, check if the provider has an isConnected() method (indicating it manages its own auth), and if so, skip the withValidToken call and invoke the callback with an empty/dummy token string:
In shared.ts around line 41-46, change withProviderToken to:
export async function withProviderToken<T>(
provider: MessagingProvider,
fn: (token: string) => Promise<T>,
): Promise<T> {
if (provider.isConnected) {
return fn('');
}
return withValidToken(provider.credentialService, fn);
}
2. (Alternative) Store the Telegram bot token under credential:telegram:access_token instead of credential:telegram:bot_token so it works with the existing OAuth flow. But this is semantically misleading and would break the isConnected() check in the adapter (assistant/src/messaging/providers/telegram-bot/adapter.ts:47).
Was this helpful? React with 👍 or 👎 to provide feedback.
| const resp = await telegram.getMe(botToken); | ||
| if (!resp.ok || !resp.result) { | ||
| return { | ||
| connected: false, | ||
| user: 'unknown', | ||
| platform: 'telegram', | ||
| metadata: { error: resp.description ?? 'getMe failed' }, | ||
| }; | ||
| } |
There was a problem hiding this comment.
🟡 testConnection has unreachable error handling because getMe throws on non-ok responses
The testConnection method at line 78 checks if (!resp.ok || !resp.result) to return a graceful { connected: false } response. However, telegram.getMe() in client.ts:35-39 already throws a TelegramApiError when !resp.ok, so the !resp.ok branch in testConnection is unreachable dead code.
Detailed Explanation
In client.ts:29-43, the getMe function does:
if (!resp.ok) {
throw new TelegramApiError(resp.status, ...);
}
return resp.json();
So when the Telegram API returns an error (e.g., invalid bot token → 401), getMe throws instead of returning a response with ok: false. The adapter's testConnection at line 77-85 expects to handle this case gracefully:
const resp = await telegram.getMe(botToken);
if (!resp.ok || !resp.result) { // ← never reached for HTTP errors
return { connected: false, ... };
}
Instead, the TelegramApiError propagates up uncaught, and the caller (messaging-auth-test.ts) catches it as a generic error, returning a raw error message instead of the structured ConnectionInfo object.
Impact: When a bot token is invalid, testConnection throws an exception instead of returning a structured { connected: false } response. The !resp.ok check on the Telegram JSON response body (which has its own ok field) is also unreachable since HTTP errors are thrown first.
| const resp = await telegram.getMe(botToken); | |
| if (!resp.ok || !resp.result) { | |
| return { | |
| connected: false, | |
| user: 'unknown', | |
| platform: 'telegram', | |
| metadata: { error: resp.description ?? 'getMe failed' }, | |
| }; | |
| } | |
| try { | |
| const resp = await telegram.getMe(botToken); | |
| if (!resp.ok || !resp.result) { | |
| return { | |
| connected: false, | |
| user: 'unknown', | |
| platform: 'telegram', | |
| metadata: { error: resp.description ?? 'getMe failed' }, | |
| }; | |
| } | |
| return { | |
| connected: true, | |
| user: resp.result.username ?? resp.result.first_name, | |
| platform: 'telegram', | |
| metadata: { | |
| botId: resp.result.id, | |
| botUsername: resp.result.username, | |
| botName: resp.result.first_name, | |
| }, | |
| }; | |
| } catch (e) { | |
| return { | |
| connected: false, | |
| user: 'unknown', | |
| platform: 'telegram', | |
| metadata: { error: e instanceof Error ? e.message : 'getMe failed' }, | |
| }; | |
| } | |
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bfbbee9c0f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| /** Return all registered providers that have stored credentials. */ | ||
| export function getConnectedProviders(): MessagingProvider[] { | ||
| return Array.from(providers.values()).filter((p) => { | ||
| if (p.isConnected) return p.isConnected(); |
There was a problem hiding this comment.
Support tokenless providers before marking them connected
By short-circuiting on p.isConnected(), Telegram is now treated as a connected provider even though the messaging tool pipeline still requires withProviderToken() to load credential:${provider.credentialService}:access_token (in assistant/src/config/bundled-skills/messaging/tools/shared.ts). Telegram setup stores credential:telegram:bot_token instead, so messaging_auth_test, messaging_send, and similar calls fail with No access token found for "telegram"; in a Telegram-only setup this makes the new provider unusable.
Useful? React with 👍 / 👎.
* fix: remove assistantId dependency from Telegram attachment delivery (#6210) Co-authored-by: Claude <noreply@anthropic.com> * feat: add Telegram webhook lifecycle reconciliation (#6211) Co-authored-by: Claude <noreply@anthropic.com> * feat: auto-configure gateway routing for single-assistant mode and add rejection visibility (#6212) Co-authored-by: Claude <noreply@anthropic.com> * feat: add Telegram Bot messaging provider for proactive outbound sends (#6222) Co-authored-by: Claude <noreply@anthropic.com> * feat: harden /deliver/telegram auth and align docs with Telegram capabilities (#6238) Co-authored-by: Claude <noreply@anthropic.com> * fix: correct misleading comment in Telegram attachment download path (#6241) Co-authored-by: Claude <noreply@anthropic.com> * fix: bound rejection notice cache with periodic eviction (#6242) Co-authored-by: Claude <noreply@anthropic.com> * fix: support tokenless providers in withProviderToken and fix testConnection error handling (#6244) Co-authored-by: Claude <noreply@anthropic.com> * fix: always reconcile webhook and normalize ingress URL (#6245) Co-authored-by: Claude <noreply@anthropic.com> * fix: resolve gateway lint error and credential security allowlist for Telegram adapter (#6257) Co-authored-by: Claude <noreply@anthropic.com> * fix: require webhook_secret in Telegram isConnected check (#6259) Co-authored-by: Claude <noreply@anthropic.com> * fix: only default routing policy in single-assistant deployments (#6261) Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
Add a Telegram Bot messaging provider following the existing Slack/Gmail provider pattern. Registers in daemon lifecycle and enables proactive outbound message sending to known Telegram chat IDs via the gateway /deliver/telegram endpoint. Part of #6200.