Skip to main content

kestrel-channel-telegram

Telegram channel for Kestrel agents — an isolated-venv feature where the agent talks over a Telegram bot it owns (a Bot API token), not a device grafted onto a personal account.

Unlike the WhatsApp channel (a companion device linked to a personal number), Telegram gives the agent its own identity with almost no friction:

  • Create a bot with @BotFather → you get a token. No phone number, no QR, no linked device.
  • You DM the bot; the agent replies as the bot.
  • Access is default-deny: with an empty allowed_senders the bot replies to no one. Obtain the user's immutable numeric Telegram ID through an operator-controlled path, then add it to allowed_senders to enable the conversation. Unauthorized /start and /id are intentionally dropped.

Setup — the agent walks you through it

You don't need to remember these steps: enable the feature and ask the agent to set up Telegram. It calls telegram_status, which returns a plain-language next_step, and guides you through whichever step is next. The full flow:

  1. Create the bot (only you can do this — it's in your Telegram):
    • Open Telegram, search @BotFather (the blue-check official one), press Start.
    • Send /newbot, choose a name (e.g. Emma) and a username ending in bot (e.g. emma_kestrel_bot).
    • BotFather replies with a token like 12345678:AAExample… (~46 chars). Copy it. (It's the bot's password — keep it private; it's fine to give to the agent/operator to store in config.)
  2. Configure the token: save the token in this feature's config (token) via the features UI (or hand it to the operator). The agent tells you when it's set and connected (as your bot's @username).
  3. Get on the allowlist (this is how you get access — default-deny):
    • Ask the agent for the link, or run telegram_link → it returns https://t.me/<your_bot>.
    • Open it to confirm you have reached the right bot. Obtain your numeric Telegram ID through an operator-controlled path.
    • Add that numeric ID to allowed_senders. Usernames are not accepted: they are mutable, while authorization uses the sender's immutable ID.
  4. Chat: DM the bot — the agent replies. Done.

What the agent does vs. what you do. The agent can't create your bot or read your Telegram, so creating the bot and obtaining your numeric Telegram ID are operator/user steps. Everything else — telling you exactly what's missing (telegram_status.next_step) and handing you the t.me link (telegram_link) — the agent drives. Setting the token/allowlist config today goes through the features UI/operator; an agent-side feature_configure tool would let the agent do that for you too (planned).

How it works

The package has two roles (same shape as kestrel-channel-whatsapp):

  • Discovery marker TelegramFeature (SDK-only) — installed into the host.
  • Transport service kestrel_channel_telegram.service — its only dep is httpx (the [service] extra), installed by the host loader into a per-agent isolated venv and run as the kestrel-telegram-service console script over the SDK stdio JSON-RPC contract.

The service registers telegram_send, telegram_status, and telegram_link. Telegram 0.1.3 requires Kestrel Core 0.53.0+ before startup: Core injects the acknowledged-inbound capability during the isolated-service handshake, and the child fails clearly before polling if it is absent. Release Core first, then Telegram; the package intentionally does not depend on Core because the feature must not invert the framework → feature dependency direction. Its active ingress is polling (the default): it removes any configured Telegram webhook and long-polls the Bot API (getUpdates), delivering text messages from allowed senders as channel.inbound and sending via sendMessage. auto is a backwards-compatible deterministic alias for polling.

Hosted Telegram delivery contract (for the Frinz host integration)

transport: webhook is intentionally absent from feature config and rejected from both config and environment fallback. A user cannot select hosted delivery, set route readiness, or pass a URL/secret into this child. The only ordinary states are polling and its auto alias.

This 0.1.7 package advertises five Telegram-specific private SDK 0.36+ host-ingress operations, consumed by Core 0.53.0+ and never exposed as agent tools. Frinz owns the concrete hosted route; durable scheduled-outbound custody and host deduplication remain separate follow-up work:

private operation host payload effect
telegram-hosted-activate {"scope":{"agent_id":"…","tenant_id":"…"},"bot_id":"…","chat_id":"…","activation_generation":"…"} Binds the canonical public bot id, signed-decimal chat id, exact immutable scope, and host-persisted activation generation, then stops and reaps polling before success. If a child token is configured, its bot id must match; a tokenless child may receive but cannot send. Retrying the same generation is safe; another generation conflicts while active.
telegram-update activation scope, bot id, chat id, activation_generation, and one authenticated Telegram update Accepted only while that exact generation binding is active. An update for another chat is terminally dropped. It returns a stable dedupe identity and deliver, dropped, or ignored disposition; an unauthorized command never creates a reply action.
telegram-hosted-deactivate activation scope, bot id, chat id, activation_generation, plus "webhook_deleted": true Accepts only the exact active generation and the host's deletion attestation, then restores polling. A completed old deactivation replay cannot clear a newer generation.
telegram-polling-ack {"dedupe_key":"telegram:v2:bot:<bot_id>:update:<update_id>","attempt_token":"…"} Releases an allowed polling callback only after Core completed/durably accepted that exact callback attempt; an opaque per-callback token prevents a delayed old ACK from acknowledging a newer redelivery.
telegram-polling-nack {"dedupe_key":"telegram:v2:bot:<bot_id>:update:<update_id>","attempt_token":"…"} Releases that exact polling callback attempt for retry after retryable Core/cognition failure. It leaves the Bot API offset unchanged; delayed completions from an earlier attempt are rejected without affecting the current future.

For rolling upgrades, 0.1.7 still accepts the 0.1.6 lifecycle envelope without chat_id. Such a legacy binding may continue receiving its already-host-scoped route, but hosted outbound is fail-closed until the host reactivates it with an immutable destination. A destination-bound activation cannot be downgraded by a legacy update or deactivation envelope.

The child retains completed activation generations for its entire process lifetime, but process-local memory is not a cross-restart replay defense. The host route provisioner must durably record every spent activation_generation and reject it after a child or host restart before it can reach telegram-hosted-activate.

The trusted HTTP host must reject a declared or streamed request body above 1 MiB with HTTP 413 before child dispatch. After JSON parsing it must accept only an object with at most 32 nested containers, 4096 aggregate collection members, and 131072 characters in any string value or object key; violations are HTTP 400. The host must authenticate Telegram's secret before reading the body, durably deduplicate the returned bot/update identity before HTTP 200, confirm the exact callback URL with getWebhookInfo after setWebhook, and confirm an empty callback URL with getWebhookInfo after deleteWebhook.

The service also advertises Core's generic private external-ingress-quiesce / external-ingress-resume lifecycle capability. Before a config transition Core asks the poller to cancel/reap getUpdates and finish any already-emitted callback, then closes its event gate. If the old child remains after a failed or live-applied transition, Core sends the exact matching resume call while that gate is still closed. The child returns the resume response before it can restart polling or produce another callback; Core then reopens the gate after receiving that response. This preserves the closed-gate handoff even for a newly-created disconnected adapter, which is restarted through its polling supervisor. This never changes hosted-route ownership and never deletes a webhook or pending Telegram updates.

Polling uses two private completions, telegram-polling-ack and telegram-polling-nack. After it emits an allowed channel.inbound notification, the child waits for Core to complete its scoped inbound handler and issue exactly one completion with the canonical bot_id + update_id dedupe key and that callback's opaque attempt token. ACK advances the Bot API offset only after durable acceptance; NACK releases the callback, leaves the offset unchanged, and makes the poller back off/redeliver. These callbacks are not agent tools and the host must dispatch them after the event handler returns (as separate JSON-RPC tasks), because event reading and private-ingress responses share the SDK stream. Hosted webhook delivery remains host-owned durable admission and does not use this child polling-completion protocol.

For a routable text reply, normalization carries Telegram's chat-local reply_to_message_id both in the private ingress result and in channel.message metadata. It is present only when the reply points to the same chat as the inbound message; malformed or cross-chat reply identities are rejected. The nested replied-to message content is never copied. Hosts must still correlate the reply against their own bound bot/chat, outbound receipt, workflow run, and post-send timestamp before treating an OK as an acknowledgement. The provider's message_date (Unix seconds) and optional edit_date are retained without content; a reply lacking a valid sent time is rejected. For an edited OK, compare the original message_date to the outbound send time—an edit after the send must not turn an earlier message into an acknowledgement.

Private ingress admission is the host attestation: no route URL, webhook secret, or user-writable readiness flag crosses into the isolated service. The child does not expose HTTP, verify Telegram's secret header, call setWebhook, or call deleteWebhook while hosted. It reports host-activated distinctly from polling and never claims route readiness; host activation merely confirms the local poller is stopped and that a trusted caller selected a bound scope.

Outbound safety in 0.1.6: a startup-attested child cannot send before the host activates its exact bot route. Once activated, the child may call sendMessage only if its configured token identifies the same bot; a tokenless or mismatched child fails before constructing a Telegram HTTP client. This also holds if activation races a send. This is not durable scheduled-send custody: Frinz still needs a user/companion/bot-scoped encrypted credential and fenced host outbound boundary before the morning-check workflow can send autonomously. Shutdown permanently latches the child closed: queued send/link/connect and host lifecycle calls cannot revive polling after a hosted binding is cleared.

The concrete Frinz host route/provisioner owns the public URL, secret-header verification, setWebhook/ deleteWebhook, durable enqueue-and-dedupe, and immutable agent/tenant/bot route record. It must generate one 256-bit route value with secrets.token_urlsafe(32), persist it as activation_generation, and use the same value on every activation, update, and deactivation call for that route. It never belongs in feature config and it must generate a new value after a completed deactivation (a completed generation cannot be reactivated). Its required order is:

  1. Start the child in polling and invoke telegram-hosted-activate with the resolved scope and bot identity. Only after it succeeds may the host install the authenticated durable route and call setWebhook.
  2. If provisioning fails, delete any partially installed webhook and invoke telegram-hosted-deactivate with the same binding and webhook_deleted: true; this is what safely resumes polling.
  3. For teardown, first call and confirm Telegram deleteWebhook, then invoke the same private deactivation payload. A mismatch or missing confirmation is rejected and leaves hosted mode latched, avoiding dual consumers.

After authenticating the Telegram secret, the host invokes telegram-update, then atomically durable-dedupes/enqueues any returned deliver event before acknowledging Telegram. The child uses a canonical bot_id + update_id event ID in both polling and hosted mode (not Telegram's chat-local message_id and not host scope), and retains raw message_id in metadata. Valid Telegram Messages without from (such as channel posts) are safely ignored with a 2xx disposition; they are never authorized or routed to the agent.

Config

key meaning
enabled turn the channel on/off
token Bot API token from @BotFather (the agent's identity)
transport polling (default) or auto (a deterministic polling compatibility alias)
allowed_senders Canonical positive decimal Telegram user IDs allowed to DM the agent. Empty = deny all. Usernames are not supported.

Webhook route URL, secret, bot identity, readiness, and agent/tenant binding remain outside feature config and outside isolated-service initialize/config-transition RPCs. They are passed only in the private host-ingress payloads above after host resolution and authentication.

Development

uv run --no-project --with "kestrel-sovereign==0.53.0" --with httpx \
  --with pytest --with pytest-asyncio python -m pytest -q

Release

Bump version in pyproject.toml (+ uv.lock), tag vX.Y.Z; publish.yml verifies the tag matches and publishes to PyPI via OIDC trusted publishing (environment: pypi).

License

MIT

Metadata

Release files for kestrel-channel-telegram 0.1.7

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for kestrel-channel-telegram 0.1.7
File Size Uploaded
kestrel_channel_telegram-0.1.7.tar.gz 83.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for kestrel-channel-telegram 0.1.7
File Interpreter ABI Platform
kestrel_channel_telegram-0.1.7-py3-none-any.whl Python 3 none any Details

Total release size: 130.4 kB

Release files / kestrel_channel_telegram-0.1.7.tar.gz

Download URL kestrel_channel_telegram-0.1.7.tar.gz
Size 83.6 kB
Tags Source
SHA-256 checksum
How to use checksums
388f4749e9dd10efe9afcfebef9e3855acb1d1d3e0d30e53afc0cea6860c709d
BLAKE2b-256 checksum
How to use checksums
11625258b13bd1f516a5a7cef3fdda45f60c21dfc777e7d293a309bfb0534c2d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.

Transparency log

Release files / kestrel_channel_telegram-0.1.7-py3-none-any.whl

Download URL kestrel_channel_telegram-0.1.7-py3-none-any.whl
Size 46.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
6dff1995a657b35db19f189f12ca436da45d6354931662d76bf90945af3be6f0
BLAKE2b-256 checksum
How to use checksums
b30f5f3aed0a8f49c06f0c05b15d082d92e71c0e4c20d3b154bc54eb6b81f2fc
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.

Transparency log

Release history Release notifications | RSS feed

0.1.8

2 release files

This release

0.1.7 This release

2 release files

0.1.6

2 release files

0.1.5

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page