diff --git a/assistant/src/config/vellum-skills/telegram-setup/SKILL.md b/assistant/src/config/vellum-skills/telegram-setup/SKILL.md index 68d5fd51e24..64155fa2a84 100644 --- a/assistant/src/config/vellum-skills/telegram-setup/SKILL.md +++ b/assistant/src/config/vellum-skills/telegram-setup/SKILL.md @@ -78,12 +78,22 @@ Use `credential_store` twice to securely save the credentials: 2. **Store the webhook secret:** - action: `store`, service: `telegram`, field: `webhook_secret`, value: the generated secret -### Step 6: Report Success +### Step 6: Validate Routing Configuration + +Verify that the gateway routing is configured to deliver inbound messages to the assistant: + +- In **single-assistant mode** (the default local deployment), routing is automatically configured. The CLI sets `GATEWAY_UNMAPPED_POLICY=default` and `GATEWAY_DEFAULT_ASSISTANT_ID` to the current assistant's ID when starting the gateway, so no manual routing configuration is needed. +- In **multi-assistant mode**, the operator must set `GATEWAY_ASSISTANT_ROUTING_JSON` to map specific chat IDs or user IDs to assistant IDs, or configure a default assistant via `GATEWAY_DEFAULT_ASSISTANT_ID` with `GATEWAY_UNMAPPED_POLICY=default`. + +If routing is misconfigured, inbound Telegram messages will be rejected and the gateway will send a visible notice to the chat explaining the issue (rate-limited to once per 5 minutes per chat). + +### Step 7: Report Success Summarize what was done: - Bot verified: @username (ID: nnn) - Webhook registration: handled automatically by the gateway - Bot commands registered: /new - Credentials stored securely in the vault +- Routing configuration validated -The gateway automatically detects credentials from the vault, reconciles the Telegram webhook registration, and begins accepting Telegram webhooks shortly. No manual environment variable configuration or webhook registration is needed. If the ingress URL or secret changes later, the gateway will automatically re-register the webhook. +The gateway automatically detects credentials from the vault, reconciles the Telegram webhook registration, and begins accepting Telegram webhooks shortly. In single-assistant mode, routing is automatically configured — no manual environment variable configuration or webhook registration is needed. If the ingress URL or secret changes later, the gateway will automatically re-register the webhook. diff --git a/cli/src/lib/local.ts b/cli/src/lib/local.ts index ba0d12cb33c..315671ac3e8 100644 --- a/cli/src/lib/local.ts +++ b/cli/src/lib/local.ts @@ -4,7 +4,8 @@ import { createRequire } from "module"; import { homedir } from "os"; import { dirname, join } from "path"; -import { GATEWAY_PORT } from "../lib/constants"; +import { loadLatestAssistant } from "./assistant-config.js"; +import { GATEWAY_PORT } from "./constants.js"; const _require = createRequire(import.meta.url); @@ -286,11 +287,20 @@ export async function startGateway(): Promise { console.log("🌐 Starting gateway..."); const gatewayDir = resolveGatewayDir(); + // Auto-configure routing for single-assistant local deployments so that + // inbound Telegram messages are forwarded without manual env var setup. + const defaultAssistantId = + process.env.GATEWAY_DEFAULT_ASSISTANT_ID + || loadLatestAssistant()?.assistantId + || "default"; + const gatewayEnv: Record = { ...process.env as Record, GATEWAY_RUNTIME_PROXY_ENABLED: "true", GATEWAY_RUNTIME_PROXY_REQUIRE_AUTH: "false", RUNTIME_HTTP_PORT: process.env.RUNTIME_HTTP_PORT || "7821", + GATEWAY_UNMAPPED_POLICY: process.env.GATEWAY_UNMAPPED_POLICY || "default", + GATEWAY_DEFAULT_ASSISTANT_ID: defaultAssistantId, }; const workspaceIngressPublicBaseUrl = readWorkspaceIngressPublicBaseUrl(); const ingressPublicBaseUrl = diff --git a/gateway/src/http/routes/telegram-webhook.ts b/gateway/src/http/routes/telegram-webhook.ts index dac2bf06f73..b1a4ab08447 100644 --- a/gateway/src/http/routes/telegram-webhook.ts +++ b/gateway/src/http/routes/telegram-webhook.ts @@ -26,6 +26,21 @@ export function buildTelegramTransportMetadata(): { hints: string[]; uxBrief: st }; } +// Rate limiter for routing rejection notices — at most one reply per chat +// within the cooldown window to avoid spamming the user. +const REJECTION_NOTICE_COOLDOWN_MS = 5 * 60 * 1000; // 5 minutes +const rejectionNoticeTimestamps = new Map(); + +function shouldSendRejectionNotice(chatId: string): boolean { + const now = Date.now(); + const lastSent = rejectionNoticeTimestamps.get(chatId); + if (lastSent !== undefined && now - lastSent < REJECTION_NOTICE_COOLDOWN_MS) { + return false; + } + rejectionNoticeTimestamps.set(chatId, now); + return true; +} + export function createTelegramWebhookHandler(config: GatewayConfig) { const dedupCache = new DedupCache(); @@ -207,7 +222,24 @@ export function createTelegramWebhookHandler(config: GatewayConfig) { replyCallbackUrl: `http://127.0.0.1:${config.port}/deliver/telegram`, }); - if (!result.forwarded && !result.rejected) { + if (result.rejected) { + log.warn( + { chatId, reason: result.rejectionReason }, + "Routing rejected inbound Telegram message", + ); + if (shouldSendRejectionNotice(chatId)) { + sendTelegramReply( + config, + chatId, + "\u26a0\ufe0f This message could not be routed to an assistant. Please check your gateway routing configuration.", + ).catch((err) => { + log.error({ err, chatId }, "Failed to send routing rejection notice"); + }); + } + return respond({ ok: true }); + } + + if (!result.forwarded) { log.error({ updateId: payload.update_id }, "Failed to forward inbound event"); if (updateId !== undefined) dedupCache.unreserve(updateId); return Response.json({ error: "Internal error" }, { status: 500 });