Adds the governing intent, the R-1 contract and descriptor, the adapter's connector contract, the observation and redaction configuration, a runbook, and the seed evidence for the first experiment: a measured pressure record and two competing hypotheses about how a long hall entry should reach a reader. The interface is declared at FLUID-2 deliberately. It publishes under HelixForge's name to an audience that did not consent to being experimented on carelessly, and the cost of a bad post is reputational rather than recoverable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014KmVxhJ35tCo7rE7UnLwWu Assistant: claude-code Assistant-Model: opus Assistant-Process: 1116572@bnt-lap001 Assistant-Session: 8ba9bb93-a72a-4883-b189-2499cce5c400
100 lines
4.2 KiB
Markdown
100 lines
4.2 KiB
Markdown
# Telegram adapter — connector capability contract
|
|
|
|
The adapter is an ordinary HTTP service. fluid-core sits in front of it and
|
|
asks nothing of it beyond the endpoints below, so it may be written in whatever
|
|
suits the platform — grammY on Node is the obvious choice for the Telegram Bot
|
|
API.
|
|
|
|
## What the adapter owns
|
|
|
|
- Rendering a hall entry into Telegram's message forms.
|
|
- Talking to the Telegram Bot API.
|
|
- Custody of the bot token for the duration of a call.
|
|
- Idempotency: recognising an `entry_id` it has already published.
|
|
|
|
## What the adapter must not do
|
|
|
|
- Create, rename or delete channels, groups or bots.
|
|
- Change membership, administrators or permissions.
|
|
- Post to any chat other than the one it is configured with.
|
|
- Edit entry content to make it fit. If an entry cannot be published
|
|
faithfully, refuse it — that refusal is the pressure signal.
|
|
- Log, echo or forward the bot token, including in error messages.
|
|
|
|
## Endpoints
|
|
|
|
Exactly the three operations in `contracts/r1.openapi.yaml`, at the same paths.
|
|
The gateway validates requests against that contract before the adapter sees
|
|
them, so the adapter may assume shape but must still assume hostile content.
|
|
|
|
## Backend capability contract
|
|
|
|
| Property | Value |
|
|
|---|---|
|
|
| Backend identity | Telegram Bot API (`api.telegram.org`) |
|
|
| Authentication | Bot token, injected at start from OpenBao; never in the image |
|
|
| Timeout | 30s per Telegram call |
|
|
| Retry | On 429 only, honouring `retry_after`. Never retry a `sendMessage` that may have succeeded |
|
|
| Circuit breaker | 5 consecutive failures opens for 60s |
|
|
| Rate limits | ~20 messages/minute to a channel; ~30 requests/second overall |
|
|
| Error mapping | 429 → 502 with `retry_after` preserved; 400 → 400; network failure → 502 |
|
|
| Tenant propagation | None; single tenant |
|
|
|
|
## The idempotency requirement
|
|
|
|
`POST /v1/hall-entries` is idempotent on `entry_id`. The caller is automation
|
|
that may be retried, and a duplicate hall entry in the channel is not something
|
|
you can quietly undo — subscribers have already seen it.
|
|
|
|
The adapter keeps its own mapping from `entry_id` to the Telegram message ids it
|
|
produced. On a repeat call it returns `200` with the existing publication rather
|
|
than posting again. If the entry content has changed, it edits the existing
|
|
messages rather than posting new ones.
|
|
|
|
This state is the adapter's, not fluid-core's. The evidence store records that a
|
|
publication happened; the adapter records which Telegram messages implement it.
|
|
|
|
## Rendering rules
|
|
|
|
Telegram formatting is not Markdown. Entry Markdown must be converted, not
|
|
passed through.
|
|
|
|
| Entry construct | Telegram |
|
|
|---|---|
|
|
| `# Heading` | Bold line |
|
|
| `**bold**`, `_italic_` | `<b>`, `<i>` in HTML parse mode |
|
|
| Fenced code | `<pre>` |
|
|
| Tables | Flattened to lines; Telegram has no table |
|
|
| Links | `<a href>` |
|
|
| Frontmatter | Stripped; `display_name` and `recorded_at` surfaced in the header |
|
|
|
|
Anything the converter cannot represent faithfully must fail the publication
|
|
rather than degrade it silently. `entry_content_loss` is a hard guardrail in
|
|
both competing hypotheses, and it can only be honest if the adapter refuses
|
|
rather than approximates.
|
|
|
|
## Telemetry the adapter should emit
|
|
|
|
The gateway observes the request path already. The adapter adds what only it can
|
|
see, by POSTing to the control plane's `/control/v1/telemetry`:
|
|
|
|
- `messages_per_entry` — how many Telegram messages one entry became.
|
|
- `entry_content_loss` — whether the conversion dropped anything.
|
|
- Telegram rate-limit encounters, as error events with class `backend_failure`.
|
|
|
|
It must not emit subscriber identity in any form. Engagement metrics
|
|
(`read_through_rate`, `reaction_rate`) come from Telegram's channel statistics
|
|
as aggregates, never per-reader.
|
|
|
|
## Configuration
|
|
|
|
| Variable | Meaning |
|
|
|---|---|
|
|
| `TELEGRAM_BOT_TOKEN` | Injected from OpenBao at start |
|
|
| `TELEGRAM_CHANNEL_ID` | The channel this adapter publishes to |
|
|
| `FLUID_CONTROL_URL` | Where to POST telemetry |
|
|
| `HALL_BASE_URL` | Base URL for links back to hall-of-helix |
|
|
|
|
One adapter instance serves one channel. The private test channel and the public
|
|
channel are separate instances with separate configuration, which is what keeps
|
|
a test publication from reaching subscribers by accident.
|