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:
parent
87795446e2
commit
49b7458dfe
13 changed files with 3010 additions and 1 deletions
100
docs/adapter-contract.md
Normal file
100
docs/adapter-contract.md
Normal 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
70
docs/observation.md
Normal 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
76
docs/runbook.md
Normal 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue