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.3 package advertises five Telegram-specific private SDK 0.36+ host-ingress operations, consumed by Core 0.53.0+ and never exposed as agent tools, so the next Frinz ticket can add the concrete integration without another package release:

private operation host payload effect
telegram-hosted-activate {"scope":{"agent_id":"…","tenant_id":"…"},"bot_id":"…","activation_generation":"…"} Validates the canonical public bot id against the configured token, binds the exact immutable scope and host-persisted activation generation, then stops and reaps polling before success. Retrying the same generation is safe; another generation conflicts while active.
telegram-update activation scope, bot id, activation_generation, and one authenticated Telegram update Accepted only while that exact generation binding is active. 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, 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.

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

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.

The concrete Frinz host route/provisioner is the next integration layer. It must own 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

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

kestrel_channel_telegram-0.1.4.tar.gz (79.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

kestrel_channel_telegram-0.1.4-py3-none-any.whl (44.3 kB view details)

Uploaded Python 3

File details

Details for the file kestrel_channel_telegram-0.1.4.tar.gz.

File metadata

  • Download URL: kestrel_channel_telegram-0.1.4.tar.gz
  • Upload date:
  • Size: 79.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for kestrel_channel_telegram-0.1.4.tar.gz
Algorithm Hash digest
SHA256 c4d6ee39bc34c0815c46c8c6455288f8d35ba0c8532b7e1913d23b8ec7cb9563
MD5 d6b06d737a5f3663d7a18098d5708835
BLAKE2b-256 c0580ed2b0eccc29851e08fd64867b7322df295ccd2dc2f2828d1b9b5e4c2f99

See more details on using hashes here.

Provenance

The following attestation bundles were made for kestrel_channel_telegram-0.1.4.tar.gz:

Publisher: publish.yml on KestrelSovereignAI/kestrel-channel-telegram

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file kestrel_channel_telegram-0.1.4-py3-none-any.whl.

File metadata

File hashes

Hashes for kestrel_channel_telegram-0.1.4-py3-none-any.whl
Algorithm Hash digest
SHA256 af16a1a2e43c5d2761b099d8fa81e7d84824f41163b20851b78d95db7ec579d3
MD5 e5a64e887cb4919c5f5b2884794d58f8
BLAKE2b-256 9a16f9db9668f1f57ecb32b3d3b6207ab117bc467e493bffafb1443f59cd6bb0

See more details on using hashes here.

Provenance

The following attestation bundles were made for kestrel_channel_telegram-0.1.4-py3-none-any.whl:

Publisher: publish.yml on KestrelSovereignAI/kestrel-channel-telegram

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.1.4 This release

2 files

0.1.3

2 files

0.1.1

2 files

0.1.0

2 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