Skip to content

feat(s071): the Telegram gateway, and two fixes to the gateway core - #120

Open
HackTuah wants to merge 2 commits into
mainfrom
slice/071-gateway-telegram
Open

HackTuah wants to merge 2 commits into
mainfrom
slice/071-gateway-telegram

Conversation

@HackTuah

@HackTuah HackTuah commented Oct 8, 2026

Copy link
Copy Markdown
Member

Slice 071, the Telegram gateway, plus two security fixes to slice 070's gateway core. The slice stays in_progress until the owner's live run with a real bot token (steps in the slice's PROOF).

Read first: two fixes to 070's approved core

  • F1, the pairing code leaked to strangers. Router.prompt_for/1 sent an unknown sender the code they were supposed to read on /gateways, so anyone who found the bot could pair by echoing it back. 070's own test asserted the leak. The prompt now says where the code is, never what it is. This restores docs/07-security-model.md's rule, "shown on /gateways and nowhere else". Nothing was exposed before this PR, because no gateway was ever started.
  • F2, ambiguous short approval ids. UUIDv7 ids share their 8-character prefix for about 65 seconds, so /approve <short> could decide the wrong request. Ambiguous prefixes are now refused, and buttons carry the full id.

The gateway

  • Bot API over Req, which is already in the lock. telegex has only an old release candidate, and ex_gram's in-memory offset fails AC5 by construction.
  • Token handling: the token is read on every call and never stored. Errors never carry the URL, which contains the token, and Req's retries are off. The log filter and the secret scanner learn the token's shape.
  • MarkdownV2: conversion that only emits closed entities, chunked after conversion. Two properties check it against an independent parse checker.
  • Delivery:
    • edits throttled to at least 1 s, keeping the final text;
    • long polling with the offset persisted before routing (at most once);
    • photos fetched only after admission, size-capped and magic-checked, stored by digest;
    • approval buttons only under the channel cap.
  • Trinity.Gateways.Supervisor with TRINITY_GATEWAYS. Nothing in the app started a gateway before this slice.
  • Images reach the session row and a :vision model, or are replaced by a note for any other model.

Criteria

AC State
AC4 formatting, escaping, chunking proven, including properties
AC5 crash and restart, no duplicate proven
AC1, AC2, AC3, AC6 automatic halves proven against the fake; live runs queued for the owner

Evidence

  • Author's gate: exit 0, 1015 passed (22 properties, 993 tests), 18 excluded, prod_check: PASS.
  • Reviewer re-run before commit: the 10 changed and new test files, 96 passed (2 properties, 94 tests). mix format --check-formatted OK; mix credo --strict --all found no issues.

Merge note

Slice 072 (Mattermost) builds on the same core and is aligning with this PR's supervisor and adapter shapes. Merge this one first.

Slice 071. Every automatic criterion runs against a local fake Bot API.
The live run needs the owner's bot token and is queued with exact steps.

Two security defects in slice 070's core, each reproduced red first:
- F1. Router.prompt_for/1 sent an unknown sender the very pairing code it
  was meant to read on /gateways, so anyone who found the bot could pair
  by echoing the prompt back. 070's own test asserted the leak. The
  prompt now says where the code is, never what it is, restoring
  docs/07's rule ("shown on /gateways and nowhere else").
- F2. Approval ids are UUIDv7, so the 8-character short id is shared by
  every request raised within about 65 seconds, and /approve <short>
  decided whichever came first. Commands.find_pending/1 now refuses an
  ambiguous prefix, and buttons carry the full id.

The gateway:
- Bot API calls over Req, already in the lock: telegex has only an old
  release candidate and ex_gram keeps its offset in memory, so neither
  meets AC5. The token is read on every call and never stored; errors
  never carry the URL, which holds it; Req's retry step is off.
- MarkdownV2 conversion that only emits a closed entity, with chunking
  checked after conversion and properties against an independent parse
  checker.
- An outbox that throttles edits to at least 1 s and keeps the final
  text; long polling with the offset persisted before routing (at most
  once); photos fetched only after admission, size-capped and
  magic-checked, stored by digest; approval buttons only under the
  channel cap.
- Trinity.Gateways.Supervisor starts the configured gateways, chosen by
  TRINITY_GATEWAYS; nothing in the app started a gateway before.
- Images reach a session row and a :vision model, and are replaced by a
  note otherwise. The log filter and secret scanner learn the token's
  shape.

Red evidence R1 to R10 is in the slice NOTES.

Signed-off-by: Ayla Croft <aylacroft@proton.me>
Signed-off-by: Ayla Croft <aylacroft@proton.me>

# Conflicts:
#	CHANGELOG.md
#	lib/trinity/gateways.ex
#	lib/trinity/gateways/router.ex
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