Prepare the FLUID interface for HelixForge Telegram publishing

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
This commit is contained in:
tegwick 2026-09-04 11:07:11 +02:00
parent 87795446e2
commit 49b7458dfe
13 changed files with 3010 additions and 1 deletions

100
docs/adapter-contract.md Normal file
View file

@ -0,0 +1,100 @@
# 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.

70
docs/observation.md Normal file
View file

@ -0,0 +1,70 @@
# Cohorts, telemetry and redaction
## Cohorts
Four populations, deliberately coarse. Blueprint §6.3 asks for cohorts stable
enough to compare over time and no more granular than the analysis requires.
| Cohort | Assigned by | Why it is distinct |
|---|---|---|
| `hall-publishing-jobs` | `X-FLUID-Consumer` prefix `hall-` | Batch behaviour; publishes many entries at once |
| `helix-forge-agents` | `X-FLUID-Consumer` prefix `agent-` | One entry at a time, at session end; the richest pressure signal |
| `operators` | `X-FLUID-Consumer` prefix `op-` | Rare, deliberate, often corrective |
| `telegram-subscribers` | Not a caller | An observed population, never an API consumer |
`telegram-subscribers` deserves care. It appears in experiment allocation and in
engagement metrics, but it never authenticates and never appears as
`ConsumerRef`. Treating readers as consumers would be the first step toward
observing them individually.
## Redaction
```
CohortMinimumSize: 5
RetentionDays: 365
AllowRawPayload: false
```
Beyond the fluid-core defaults, this interface adds:
- **No Telegram user identity, ever.** Canon ID-01 identities — user ids,
usernames, display names of subscribers — must not reach the evidence store
in any form, redacted or otherwise. There is no legitimate analysis that
needs them, so the safe rule is that they never arrive.
- **Entry content is not telemetry.** An entry body may appear in a request; it
must not appear in a telemetry event. What is recorded is its length, its
section count, and whether conversion succeeded.
- **The bot token appears nowhere.** Not in errors, not in traces, not in
redacted form.
## The pseudonymization salt
Generate once, store in OpenBao alongside the bot token, inject as
`FLUID_REDACTION_SALT`:
```bash
openssl rand -hex 32
```
It must never change. Rotating it makes every previously-seen consumer look
new, which silently invalidates every longitudinal comparison the interface has
accumulated. If it is ever compromised, the correct response is to accept the
loss and start a fresh evidence baseline, not to rotate quietly.
## Metrics
| Metric | Role | Source |
|---|---|---|
| `read_through_rate` | primary | Telegram channel statistics, aggregate |
| `messages_per_entry` | guardrail | Adapter |
| `entry_content_loss` | guardrail | Adapter |
| `error_rate` | guardrail | Gateway |
| `reaction_rate` | secondary | Telegram channel statistics, aggregate |
| `time_to_first_read` | learning | Telegram channel statistics, aggregate |
`read_through_rate` is the one to be careful about. Telegram reports view counts
per message, not per reader. For the teaser form it is link clicks over views;
for the serialized form it is views of the last part over views of the first.
Those are not the same measurement, and comparing them directly is the weakest
part of the first experiment. Say so in the experiment's amendment record rather
than letting the comparison look cleaner than it is.

76
docs/runbook.md Normal file
View file

@ -0,0 +1,76 @@
# Runbook — helix-forge-telegram-publishing
## Rotating the bot token
1. In BotFather: `/revoke` for the bot, which issues a new token.
2. Write the new token to OpenBao at the interface's secret path.
3. Restart the adapter. It reads the token at start only.
4. Confirm with one publication to the **private test channel**.
The old token stops working the moment it is revoked, so publications in flight
will fail. Do this when nothing is publishing.
## Recovering channel administration
If the bot loses its administrator rights, it cannot post and every publication
fails with a Telegram `400`.
1. In the channel's administrator settings, confirm the bot is present with
**Post Messages** enabled.
2. Grant nothing else. The bot does not need to edit others' messages, delete
messages, or manage members, and the intent forbids it from using such
rights if it had them.
3. If the bot was removed entirely, re-add it and re-run one test publication.
A human must do this. The Daimon is forbidden from changing channel membership
or permissions (`InterfaceEvolutionIntent.md` §7).
## Rolling back a revision
Publish a routing policy with a higher generation naming the previous revision
as default:
```bash
fluid experiment stop <experiment-id> --generation N --default-revision R-1 \
--reason "..." --policy-out rollback.json
fluid policy put --file rollback.json
```
If no experiment is running, write the policy by hand with `default_revision`
set to the known-good revision and a generation above the current one. Rollback
is a document replacement; nothing is unwound.
Already-published Telegram messages are **not** rolled back. They were seen.
Correcting a published entry is an edit through the adapter, not a rollback.
## An entry published wrongly
1. Stop further publication: install a policy pointing at the last known-good
revision.
2. Correct the entry in `hall-of-helix` and re-publish it. The adapter edits the
existing messages rather than posting new ones.
3. If the entry cannot be corrected in place — wrong entry entirely, or content
that should not have been published — delete the messages by hand in
Telegram and record why in the publication's audit trail.
4. Record a pressure observation. An entry published wrongly is exactly the
evidence the framework exists to collect, and the instinct to tidy it away
is the instinct to lose it.
## Checking what happened
```bash
export FLUID_INTERFACE=helix-forge-telegram-publishing
fluid audit trace R-1 # how a revision came to exist, and what followed
fluid pressure list # what the interface is currently struggling with
fluid telemetry --limit 50 # recent traffic
fluid events --entity <entry-id> # one publication's history
```
## When the evidence store is unavailable
Publication continues. The gateway serves from cached configuration and buffers
telemetry, dropping it if the buffer fills. You lose observation, not service.
Do not promote anything while the store is down: the promotion would be
unauditable, which is the one thing this framework will not trade away.