Skip to content

feat(channels): hosted (Agenta-owned) Telegram bot - #6724

Open
mmabrouk wants to merge 16 commits into
channels/telegramfrom
channels/telegram-hosted
Open

feat(channels): hosted (Agenta-owned) Telegram bot#6724
mmabrouk wants to merge 16 commits into
channels/telegramfrom
channels/telegram-hosted

Conversation

@mmabrouk

@mmabrouk mmabrouk commented Sep 9, 2026

Copy link
Copy Markdown
Member

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)

  1. The connect UI calls 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.
  2. The user taps it and presses Start. Telegram sends /start <token> to the shared bot.
  3. The hosted ingress verifies the one deployment webhook secret, consumes the token, and binds (bot_id, chat_id) -> project, connection plus the account identity link, all in one transaction.
  4. Later messages resolve the project from the bind map and route to the agent. Egress uses the one deployment bot token.

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 (channel telegram_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.
  • ingress: ingest_telegram_event branches 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

  • 871 channels + secrets unit tests pass.
  • The bind DAO's atomic transaction was smoke-tested against Postgres (issue, consume+bind, account link, replay idempotency, conflicting-project refusal).
  • Live end to end on the channels stack with the QA Telegram account: /start connected in 0.3s; a message got the agent's answer in 9s; the DB rows (binding, consumed token, identity link attributing the invoking user) verified.

…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.
@vercel

vercel Bot commented Sep 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
agenta-documentation Ready Ready Preview Sep 9, 2026 11:07am UTC

Request Review

@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

📘 Docs preview

Status ✅ Ready
Preview https://pr-6724-agenta-docs-preview.mahmoud-637.workers.dev/docs
Inspect Actions run
Commit c34d2fd9fb91e659d1645d9ea34eb9099a51d8de

This comment updates in place on every push.

@coderabbitai

coderabbitai Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (1)
  • release/.*

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Advanced

Run ID: c1c10a2b-adac-4ab9-a034-ed9475fbdc73

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Summary

Summary by CodeRabbit

  • New Features
    • Added support for connecting projects to a hosted Telegram bot.
    • Users can generate a secure, expiring Telegram connection link from the channels settings.
    • Telegram chats can be linked to a project through a guided /start flow.
    • Messages from linked chats are routed to the appropriate project and can receive agent replies.
    • Added support for group chats and account linking through Telegram.
  • Documentation
    • Documented the hosted Telegram connection flow and end-to-end validation.

Walkthrough

Adds 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.

Changes

Hosted Telegram Bot

Layer / File(s) Summary
Hosted adapter and runtime wiring
api/oss/src/utils/env.py, api/oss/src/core/channels/adapters/telegram_hosted/*, api/entrypoints/*
Adds hosted Telegram configuration, capabilities, adapter behavior, registry registration, and conditional binding-service wiring.
Binding contracts and persistence
api/oss/src/core/channels/telegram_binding.py, api/oss/src/dbs/postgres/channels/*, api/oss/databases/postgres/migrations/core_oss/versions/*, api/oss/tests/pytest/unit/channels/telegram/test_telegram_binding.py
Adds one-time bind tokens, durable chat bindings, atomic PostgreSQL persistence, and lifecycle tests.
Hosted connection and bind-link API
api/oss/src/core/channels/service.py, api/oss/src/apis/fastapi/channels/router.py, api/oss/tests/pytest/unit/channels/test_channels_router.py
Creates hosted project connections and exposes a permission-protected bind-link endpoint.
Hosted ingress routing and validation
api/oss/src/apis/fastapi/channels/ingress.py, api/oss/tests/pytest/unit/channels/telegram/*, docs/design/channels-research/v2/overnight-progress.md
Verifies hosted webhooks, handles /start commands, routes bound messages, enqueues events, and records hosted-bot validation results.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: 🟠 High · up to a7b59

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
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning 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:… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the primary change: adding a hosted Agenta-owned Telegram bot.
Description check ✅ Passed The description directly explains the hosted Telegram bot flow, implementation components, design constraints, and testing coverage.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

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 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch channels/telegram-hosted

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@mmabrouk

mmabrouk commented Sep 9, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between bf4902b and a7b590f.

📒 Files selected for processing (20)
  • api/entrypoints/channel_adapters.py
  • api/entrypoints/routers.py
  • api/oss/databases/postgres/migrations/core_oss/versions/oss000000030_add_telegram_hosted_bind.py
  • api/oss/src/apis/fastapi/channels/ingress.py
  • api/oss/src/apis/fastapi/channels/router.py
  • api/oss/src/core/channels/adapters/telegram_hosted/__init__.py
  • api/oss/src/core/channels/adapters/telegram_hosted/adapter.py
  • api/oss/src/core/channels/adapters/telegram_hosted/capabilities.py
  • api/oss/src/core/channels/service.py
  • api/oss/src/core/channels/telegram_binding.py
  • api/oss/src/dbs/postgres/channels/telegram_bind_dao.py
  • api/oss/src/dbs/postgres/channels/telegram_bind_dbas.py
  • api/oss/src/dbs/postgres/channels/telegram_bind_dbes.py
  • api/oss/src/utils/env.py
  • api/oss/tests/pytest/unit/channels/telegram/test_telegram_binding.py
  • api/oss/tests/pytest/unit/channels/telegram/test_telegram_hosted_capabilities.py
  • api/oss/tests/pytest/unit/channels/telegram/test_telegram_hosted_env.py
  • api/oss/tests/pytest/unit/channels/telegram/test_telegram_hosted_ingress_helpers.py
  • api/oss/tests/pytest/unit/channels/test_channels_router.py
  • docs/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.

Comment thread api/oss/src/apis/fastapi/channels/router.py Outdated
Comment thread api/oss/src/core/channels/telegram_binding.py
Comment thread api/oss/src/dbs/postgres/channels/telegram_bind_dao.py Outdated
Comment thread api/oss/src/utils/env.py Outdated
- 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.
@mmabrouk

mmabrouk commented Sep 9, 2026

Copy link
Copy Markdown
Member Author

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.

@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Railway Preview Environment

Preview URL https://gateway-pr-6724.up.railway.app/w
Project agenta-oss-clone-spike
Image tag pr-6724-42cb8758
Status Deployed
Railway logs Open logs
Workflow logs View workflow run
Updated at 2026-09-09T11:08:38.329Z

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant