feat(channels): hosted (Agenta-owned) Telegram bot - #6724
Conversation
…lve) First slice of lane 3 (hosted Agenta-owned Telegram bot), per the Codex-reviewed plan. TelegramBindingService owns the one-time deep-link token and the chat-to-project binding, over an injected store Protocol. Opaque short token (Telegram 64-char deep-link cap), TTL, one-time consume, one-chat-one-project, replay-idempotent, atomic consume+bind in the store. 8 unit tests. No wiring yet; the DAO, hosted adapter, ingress seam and router follow.
- env: ChannelsTelegramConfig (TELEGRAM_HOSTED_BOT_TOKEN + TELEGRAM_HOSTED_WEBHOOK_SECRET), enabled only when both are set; exposed at env.channels.telegram. - telegram_hosted capabilities: identity keys on ["project"] (one hosted connection per project; the ingress resolves the chat via the bind map), space/thread still key on the chat, rendering/fill/addressing identical to the custom bot. No paste-a-token setup. Tests for both. The HostedTelegramAdapter and the ingress hosted-resolve seam follow as one slice (their contract is defined by that flow).
consume_bind_token now composes external_user_key from the hosted capabilities (chat id as scope, sender as user) and hands the store a ready key, so the atomic consume+bind+link writes the SAME key the inbox worker later composes and the worker finds the link instead of falling back to the agent creator. Store Protocol takes external_user_key, not sender_id. Tests assert the match.
…ion) - migration oss000000030: channel_telegram_bind_tokens (one-time codes) and channel_telegram_chat_bindings ((bot_id, chat_id) -> project, connection), both globally keyed since they are looked up without a project. - TelegramBindingDAO implements the store Protocol. consume_token_and_bind is one transaction: guarded token consume (consumed_at IS NULL), chat binding and account link both ON CONFLICT DO NOTHING, then the stored binding is read back. A concurrent /start cannot double-write. Applied to the channels stack DB and smoke-tested end to end against Postgres: issue, consume+bind, account link with the worker's key, replay idempotency, conflicting-project refusal, cleanup.
- HostedTelegramAdapter (subclasses TelegramAdapter): telegram_hosted channel, project identity, egress via the deployment token, verify via the deployment webhook secret returning the project, no-op per-project activation, verify_connection returns the deployment bot id/username into the connection data for parse_event. - ingress: ingest_telegram_event branches to a hosted path when the routing token is the deployment bot id. The hosted path verifies the deployment secret, completes a /start <token> bind (then greets), and routes any other update to the bound project via the bind map; unbound chats are ignored. - routers.py: builds the binding service (DAO + hosted capabilities) only when the hosted bot is configured, and passes it to the ingress. - registered telegram_hosted adapter; env bot_username added; tests for the ingress parsers. 766 channels unit tests green.
- ChannelsService.ensure_hosted_telegram_connection: create-or-reuse the one telegram_hosted connection for a project and attach the chosen agent as its default (idempotent). Zero grants means the agent answers in the bound chat. - router: POST /catalog/channels/telegram_hosted/bind-link/ (EDIT_CHANNELS) ensures the connection+agent then mints the one-time deep link; returns the url and its expiry. The UI renders the QR from the url client-side. 404 when the hosted bot is not configured. - binding service exposes ttl_seconds; router/ingress wired with the service. - updated the router permission-matrix and trailing-slash audits for the route.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📘 Docs preview
This comment updates in place on every push. |
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. 🗂️ Base branches to auto review (1)
Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository YAML (base), Organization UI (inherited) Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 SummarySummary by CodeRabbit
WalkthroughAdds hosted Telegram support through a shared adapter, project-scoped chat bindings, one-time deep links, PostgreSQL persistence, webhook ingress routing, and an authenticated bind-link endpoint. ChangesHosted Telegram Bot
Priority: ➖ Normal Estimated code review effort: 4 (Complex) | ~45 minutes Merge Risk: 🟠 High · up to Hosted Telegram can issue unusable bind links, route group messages to a project agent, and mishandle concurrent attempts to bind a chat already assigned elsewhere. These failures should be resolved before merge. Sequence Diagram(s)sequenceDiagram
participant TelegramClient
participant ChannelsRouter
participant TelegramBindingService
participant ChannelsIngressRouter
participant HostedTelegramAdapter
TelegramClient->>ChannelsRouter: request hosted bind link
ChannelsRouter->>TelegramBindingService: issue_bind_link
TelegramBindingService-->>ChannelsRouter: return deep link
TelegramClient->>ChannelsIngressRouter: send /start token webhook
ChannelsIngressRouter->>HostedTelegramAdapter: verify webhook secret
ChannelsIngressRouter->>TelegramBindingService: consume_bind_token
TelegramBindingService-->>ChannelsIngressRouter: return chat binding
TelegramClient->>ChannelsIngressRouter: send bound chat message
ChannelsIngressRouter->>TelegramBindingService: resolve_bound_connection
ChannelsIngressRouter->>HostedTelegramAdapter: record and enqueue event
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 20.00% which is insufficient. The required threshold is 60.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 80 functions across 19 files. (1 skipped: 1 unsupported.) ✨ Finishing Touches 💡 2📝 Generate docstrings 💡
🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@coderabbitai full review |
✅ Action performedFull review finished. |
There was a problem hiding this comment.
Actionable comments posted: 4
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository YAML (base), Organization UI (inherited)
Review profile: CHILL
Plan: Advanced
Run ID: 1c2e40a6-59e4-4056-8cf2-0ccdfb1fe473
📒 Files selected for processing (20)
api/entrypoints/channel_adapters.pyapi/entrypoints/routers.pyapi/oss/databases/postgres/migrations/core_oss/versions/oss000000030_add_telegram_hosted_bind.pyapi/oss/src/apis/fastapi/channels/ingress.pyapi/oss/src/apis/fastapi/channels/router.pyapi/oss/src/core/channels/adapters/telegram_hosted/__init__.pyapi/oss/src/core/channels/adapters/telegram_hosted/adapter.pyapi/oss/src/core/channels/adapters/telegram_hosted/capabilities.pyapi/oss/src/core/channels/service.pyapi/oss/src/core/channels/telegram_binding.pyapi/oss/src/dbs/postgres/channels/telegram_bind_dao.pyapi/oss/src/dbs/postgres/channels/telegram_bind_dbas.pyapi/oss/src/dbs/postgres/channels/telegram_bind_dbes.pyapi/oss/src/utils/env.pyapi/oss/tests/pytest/unit/channels/telegram/test_telegram_binding.pyapi/oss/tests/pytest/unit/channels/telegram/test_telegram_hosted_capabilities.pyapi/oss/tests/pytest/unit/channels/telegram/test_telegram_hosted_env.pyapi/oss/tests/pytest/unit/channels/telegram/test_telegram_hosted_ingress_helpers.pyapi/oss/tests/pytest/unit/channels/test_channels_router.pydocs/design/channels-research/v2/overnight-progress.md
Included review availability: Your plan provides up to 8 included reviews per hour; 4 remain after this review.
- P1 group sender: the hosted flow now drops non-private chats. v1 hosted is private chats only, where the sender is the chat, so attribution is exact and an unbound group member can no longer invoke the agent as the binder. - P1 concurrent-bind race: consume_token_and_bind now reads the binding back INSIDE the transaction and raises ChatBoundElsewhere on an ownership mismatch, before the account link and the commit, so a losing concurrent /start writes nothing and its token stays usable. Expiry is now in the consume predicate. Verified against Postgres under true concurrency: one wins, one refused, one binding, one link, one token consumed. - P2 fresh-link replay: a fresh token for an already-connected chat is refused (ChatAlreadyConnected) instead of a phantom success; a consumed token on the same binding stays idempotent. - P2 reconnect: ensure_hosted_telegram_connection reuses and unarchives an archived hosted connection instead of creating a duplicate that collides on the project external key. - P2 username: the feature is enabled only with a username too, and the adapter strips a leading @ so mention matching still works. - P3: the /start parser no longer crashes on whitespace-only input. - simplify: the custom ingest path now uses the shared _record_and_enqueue. Tests added for each. 872 channels+secrets unit tests pass.
Per CodeRabbit: the request/response models for the bind-link route belong with the other channels wire models, not inline in the router.
|
Addressed an independent Codex (gpt-astra, xhigh) review and CodeRabbit, all in this branch. Codex found and I fixed: a group-sender authorization gap (hosted is now private-chat only for v1), a cross-project concurrent-bind race (the atomic transaction now validates binding ownership before the account link and commit; re-proven against Postgres under true concurrency), a fresh-link phantom-success, a reconnect conflict with an archived connection, the username/deep-link gap, and a whitespace crash in the /start parser. The custom ingest path now reuses the shared record-and-enqueue helper. All 4 CodeRabbit threads resolved. 875 channels+secrets unit tests pass. The flow is live-verified end to end (tap to connect, then the agent answers) and again after the fixes. v1 is private chats only, per the plan. |
Railway Preview Environment
|
The hosted config test did not pass bot_username explicitly, so it used the ambient env default. The dev container has TELEGRAM_HOSTED_BOT_USERNAME set from live testing, which masked the failure locally; CI has no such value, so the old 'token+secret is enough' assertion failed once enabled began requiring a username. Every case now passes all three fields explicitly.
Disconnecting (archiving) a connection now frees its hosted Telegram chat bindings via TelegramBindingDAO.delete_bindings_for_connection, called from the archive endpoint (no-op for channels without bindings). A chat can then reconnect, including to a different project, instead of ChatBoundElsewhere forever. Unit-tested (release frees the chat to rebind) and DB-verified.
What
The hosted Telegram bot: one Agenta-owned bot that every project connects to with a tap, no token or BotFather. Stacked on the custom-bot PR #6679.
A custom bot is one bot per customer (keyed on bot_id). The hosted bot is one shared bot, so an update carries only the chat and user, never the project. This adds a per-project hosted connection plus a chat-to-project bind map.
How (the flow)
POST /catalog/channels/telegram_hosted/bind-link/with the chosen agent. It ensures the project's hosted connection + default agent and mints a one-time deep link (t.me/<bot>?start=<token>). The UI renders the QR from the url./start <token>to the shared bot.(bot_id, chat_id) -> project, connectionplus the account identity link, all in one transaction.Key pieces
core/channels/telegram_binding.py+dbs/postgres/channels/telegram_bind_dao.py: the bind service and its atomic store (migration oss000000030: two globally-keyed tables).adapters/telegram_hosted/: a separate adapter (channeltelegram_hosted, project identity) that reuses the custom bot's transport, with deployment-token egress, deployment-secret verify returning the project, and no-op per-project activation.ingest_telegram_eventbranches to the hosted path when the routing token is the deployment bot id.env.channels.telegram: the shared bot token, webhook secret, and username.Design
Reviewed by Codex (gpt-astra) before building; the separate-adapter shape and the dedicated bind table are its recommendations. Per Codex's simplicity cut, v1 is private-chat binding with the sender matched to the link creator; QR is rendered client-side.
Testing