Ground the repo docs and move presence provisioning to a declared spec

Align AGENTS.md with the FT-WP- prefix the hub already registered, and
rewrite INTENT.md and SCOPE.md, which were still the generated stubs and
carried none of the composed-post framing the interface is built around.

Fix two artifact inconsistencies: R-1's retry comment still described
POST /v1/hall-entries idempotent on entry_id, from before the reframe to
composed posts; and the seed pressure record listed only one of the two
hypotheses that cite it. Rename it to match its id.

Replace the manual T01-T03 route with FT-WP-0002. The Bot API cannot
create a bot or a channel, so provisioning is an MTProto client acting as
a designated operator account (Canon INT-03), driven from a declared
presence spec owned by the campaign. One bounded human bootstrap remains
and docs/provisioning.md says why it cannot be removed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0172sgCZEEDJcnQmr4SGDvKa

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 1361245@bnt-lap001
Assistant-Session: b3b428ef-f3e6-4688-b091-01f71461d66a
This commit is contained in:
tegwick 2026-09-04 19:21:08 +02:00
parent 3dae4ae74c
commit 5a785b9d9f
11 changed files with 670 additions and 46 deletions

View file

@ -7,7 +7,7 @@
**Domain:** infotech
**Repo slug:** fluid-telegram
**Topic ID:** `cee7bedf-2b48-46ef-8601-006474f2ad7a`
**Workplan prefix:** `FLUID-WP-`
**Workplan prefix:** `FT-WP-`
---
@ -136,16 +136,16 @@ curl -s -X PATCH "http://127.0.0.1:8000/tasks/<task_id>" \
Work items originate as files in this repo — not in the hub. The hub is a
read/cache/index layer that rebuilds from files.
**File location:** `workplans/FLUID-WP-NNNN-<slug>.md`
**File location:** `workplans/FT-WP-NNNN-<slug>.md`
**Archived location:** finished workplans may move to
`workplans/archived/YYMMDD-FLUID-WP-NNNN-<slug>.md`. The `YYMMDD` prefix is
`workplans/archived/YYMMDD-FT-WP-NNNN-<slug>.md`. The `YYMMDD` prefix is
the completion/archive date; the frontmatter `id` does not change.
**Ad Hoc Tasks:** small opportunistic fixes discovered during a session use
`workplans/ADHOC-YYYY-MM-DD.md`, workplan id
`FLUID-WP-ADHOC-YYYY-MM-DD`, and task ids
`FLUID-WP-ADHOC-YYYY-MM-DD-T01`, etc. `FLUID-WP` includes its final `-WP`
`FT-WP-ADHOC-YYYY-MM-DD`, and task ids
`FT-WP-ADHOC-YYYY-MM-DD-T01`, etc. `FT-WP` includes its final `-WP`
token. Unqualified historic `ADHOC-*` ids are grandfathered and must not be
copied into new records. Use this only for low-risk work completed directly;
create a normal workplan for anything needing analysis, design, approval,
@ -155,7 +155,7 @@ dependencies, or multiple phases.
```yaml
---
id: FLUID-WP-NNNN
id: FT-WP-NNNN
type: workplan
title: "..."
domain: infotech
@ -183,7 +183,7 @@ API/MCP/frontmatter bridges until `STATE-WP-0069` retires them — see
## Task Title
` ` `task
id: FLUID-WP-NNNN-T01
id: FT-WP-NNNN-T01
status: wait | todo | progress | done | cancel
priority: high | medium | low
state_hub_task_id: "<uuid>" # deterministic UUIDv5; managed by Repo Manager
@ -196,7 +196,7 @@ Status progression: `todo` → `progress` → `done`; use `wait` for waiting/blo
**Residuals when finishing:** actionable leftovers become live work records
before `status: finished` — usually an intake (`origin: residual`,
`origin_ref: FLUID-WP-NNNN`) or a spawned workplan. Residual is a *role*,
`origin_ref: FT-WP-NNNN`) or a spawned workplan. Residual is a *role*,
not a kind. Fleet list lives on State Hub, not in `SCOPE.md`.
To create a new workplan:

View file

@ -7,18 +7,53 @@ updated: "2026-09-04"
## Why it exists
Fluid interface for telegram messaging and interaction platform.
`fluid-telegram` is the FLUID interface `helix-forge-telegram-publishing`: the
deterministic outbound surface through which HelixForge material reaches
Telegram.
Fluid interface for telegram messaging and interaction platform.
It exists because publishing under an organisation's name to an audience of
strangers is a governed act, not a side effect of having a bot token. The
interface makes that act legible: every publication carries a named reviewer, a
recorded consent basis, and a citation to the work it describes, and every one
of them is reconstructible afterwards from the evidence store.
## What it is not
**It is not a content system.** The channel does not republish hall entries. A
hall entry is a considered first-person account written for a colleague reading
a repository; a channel post is short, personal and written to travel. The post
is composed *from* the entry and is a different piece of writing.
Composition therefore happens **before** this interface is called, and belongs
to the campaign repository `pr-hall-of-helix`. The boundary is architectural —
`ArchitectureBlueprint.md` §48.1 forbids an LLM in the mandatory request path —
and editorial, because a post speaks about a named person's work in HelixForge's
voice and deserves a human read before it goes out.
This repository renders and delivers what it is given. It never writes, selects,
shortens or embellishes. A post that will not fit is refused, not truncated.
## Governing principle
This repository should stay focused on the purpose above. Work that changes its
authority, ownership boundaries, or operational promises should be captured in a
workplan before implementation.
`InterfaceEvolutionIntent.md` (IEI-1) is the constitutional constraint on this
interface and takes precedence over this file where they touch. It is
subordinate in turn to `helix-forge/INTENT.md`.
Operational authority is **FLUID-2 (Advisory)** by deliberate choice: the Daimon
may observe, classify pressure and propose adaptations, but may not generate
implementations, run experiments unattended, or promote anything. Raising that
mode is a governance change under IEI-1 §20, not an operational decision.
Work that changes this repository's authority, ownership boundaries, or
operational promises is captured in a workplan before implementation.
## What it enables
- A coding agent can understand why the repository exists before changing it.
- State Hub can register and coordinate work for this repository.
- Future workplans can stay connected to the repository's intended role.
- HelixForge can publish to Telegram under a stated intent, with a contract, a
revision history, and an audit trail that reconstructs any publication.
- A campaign (`pr-hall-of-helix`) can publish through a delivery surface it does
not have to own, and receive per-variant engagement back.
- Delivery questions — attach a portrait or not, which form travels further —
can be settled by experiment rather than by preference.
- The interface is reusable by any HelixForge campaign, not only the Hall of
Helix.

View file

@ -1,32 +1,69 @@
# SCOPE
> This file was generated by `statehub register`. Refine it as the repository
> boundaries become clearer.
## One-liner
Fluid interface for telegram messaging and interaction platform.
The deterministic delivery interface that publishes composed HelixForge posts to
Telegram, under a governed contract with a reconstructible audit trail.
## Core Idea
fluid-telegram exists to provide the capability described in INTENT.md.
A caller submits a **composed post** — already written, already reviewed,
carrying a consent basis and a citation to its source entry. This interface
validates it, renders it into the Telegram form the serving revision calls for,
delivers it through the Bot API, and reports how it performed.
Everything about *what to say and when* lives elsewhere. Everything about *how
it is carried and proven* lives here.
## In Scope
- Maintain the repository's primary implementation.
- Keep docs, tests, and operational metadata current.
- The R-1 contract (`contracts/r1.openapi.yaml`) and its revisions.
- The adapter: rendering, delivery, idempotency on `post_id`, custody of the bot
token for the duration of a call.
- Validation and refusal: no reviewer, no consent basis, or a body that exceeds
the limit for the form being published → refused, never truncated.
- Delivery experiments (`E-tg-delivery`, cohort `telegram-subscribers`) and the
hypotheses behind them.
- Per-variant engagement reporting back to the campaign.
- Observation and redaction policy (`docs/observation.md`).
- Bot and channel configuration, and the provisioning of that configuration from
a declared specification (FT-WP-0002).
- Operational recovery: token rotation, channel administration, revision
rollback (`docs/runbook.md`).
## Out of Scope
- Own unrelated adjacent systems.
- Make irreversible operational decisions without human approval.
- **Composition and editorial voice.** Condensing an entry into a post, choosing
which work is worth publishing, the publishing cadence, variant strategies.
These belong to `pr-hall-of-helix`.
- **Consent records and the review queue.** The campaign obtains and holds them;
this interface only refuses a post that arrives without one.
- **Reading `hall-of-helix`.** The adapter's only knowledge of an entry is the id
and URL a post cites, which it renders as a link.
- **Autonomous administration.** Creating, renaming or deleting channels, groups
or bots, and changing membership or permissions, are forbidden to the Daimon
(IEI-1 §7). Where such acts are automated at all, they run in the separate,
human-approved provisioning plane, never in the request path.
- Owning unrelated adjacent systems.
## Current State
- Status: active; implementation and stability should be verified by the repo agent.
Design-complete, not yet live. Governance, the R-1 contract, the adapter's
connector contract, observation configuration, the seed pressure record and two
competing hypotheses are in place. The bot, the channels and the adapter are
not.
- Work: `workplans/FT-WP-0001-telegram-identity-and-hall-channel.md` (active)
- Provisioning: `workplans/FT-WP-0002-*` (declared-presence provisioning; supersedes
the manual T01T03 route)
- Depends on: `fluid-core` `FLUID-WP-0008` (handover)
- Not yet created: `pr-hall-of-helix`
## Getting Oriented
- Start with: INTENT.md
- Agent instructions: AGENTS.md
- Workplans: workplans/
- Why it exists: `INTENT.md`
- What may change and what may not: `InterfaceEvolutionIntent.md` (IEI-1)
- What the adapter owes: `docs/adapter-contract.md`
- Platform capability reference: `TelegramCapabilityCanon.md`
- Agent instructions: `AGENTS.md`
- Workplans: `workplans/`

24
WORK-RECORDS.md Normal file
View file

@ -0,0 +1,24 @@
# Work Records — fluid-telegram
> Generated by `statehub fix-consistency` (CUST-WP-0061-T04, work-record
> stage 3). Do not edit by hand — edit the source file/block listed for
> each record and re-run fix-consistency to refresh this index. Archived
> workplans are omitted; closed decisions/intakes/engagements stay listed
> so recently-resolved work is still visible. [auto]
| Kind | ID | Status | Lane | Source |
| --- | --- | --- | --- | --- |
| workplan | FT-WP-0001 | active | — | workplans/FT-WP-0001-telegram-identity-and-hall-channel.md |
| task | FT-WP-0001-T01 | todo | — | workplans/FT-WP-0001-telegram-identity-and-hall-channel.md |
| task | FT-WP-0001-T02 | todo | — | workplans/FT-WP-0001-telegram-identity-and-hall-channel.md |
| task | FT-WP-0001-T03 | todo | — | workplans/FT-WP-0001-telegram-identity-and-hall-channel.md |
| task | FT-WP-0001-T04 | todo | — | workplans/FT-WP-0001-telegram-identity-and-hall-channel.md |
| task | FT-WP-0001-T05 | todo | — | workplans/FT-WP-0001-telegram-identity-and-hall-channel.md |
| task | FT-WP-0001-T06 | todo | — | workplans/FT-WP-0001-telegram-identity-and-hall-channel.md |
| task | FT-WP-0001-T07 | todo | — | workplans/FT-WP-0001-telegram-identity-and-hall-channel.md |
| task | FT-WP-0001-T08 | todo | — | workplans/FT-WP-0001-telegram-identity-and-hall-channel.md |
| task | FT-WP-0001-T09 | todo | — | workplans/FT-WP-0001-telegram-identity-and-hall-channel.md |
| task | FT-WP-0001-T10 | todo | — | workplans/FT-WP-0001-telegram-identity-and-hall-channel.md |
| task | FT-WP-0001-T11 | todo | — | workplans/FT-WP-0001-telegram-identity-and-hall-channel.md |
| task | FT-WP-0001-T12 | todo | — | workplans/FT-WP-0001-telegram-identity-and-hall-channel.md |
| task | FT-WP-0001-T13 | todo | — | workplans/FT-WP-0001-telegram-identity-and-hall-channel.md |

165
docs/provisioning.md Normal file
View file

@ -0,0 +1,165 @@
# Declared presence — provisioning the Telegram surface
`FT-WP-0001` T01T03 were written as human steps: register a bot through
BotFather, create two channels, generate a salt. This document replaces that
route with a declared specification and a reconciler, and states honestly which
part of it cannot be automated and why.
## The constraint that shapes everything
**The Bot API cannot create a bot or a channel.** It can only act as one. Both
acts are client capabilities, reachable only through MTProto with a *user*
account — Canon INT-03, "capabilities unavailable through the Bot API".
So the provisioner is an MTProto client acting as a designated operator account.
It messages BotFather the way a person would, and it calls `channels.createChannel`
the way a client would.
Three things genuinely cannot be derived from a specification:
| Human step | Why | Frequency |
|---|---|---|
| A Telegram account for the operator | Requires a phone number and a device | Once |
| `api_id` / `api_hash` | Issued by a web form at my.telegram.org | Once |
| The login code (and 2FA password) that mints the MTProto session | Telegram sends it out of band, by design | Once, and again if the session is revoked |
Everything else — bot registration, naming, description, avatar, channel
creation, visibility, administrator rights, the redaction salt — is declared and
reconciled.
That is the honest boundary. This is not "no human steps"; it is **one bounded
bootstrap, then a declared surface that converges.** The steps that remain are
the ones Telegram places outside any API on purpose.
## Where the specification lives
The presence spec is **editorial**, not mechanical. It carries the bot's name,
its description, its avatar, and the channel titles a stranger will read. That is
brand, and brand belongs to the campaign:
```
pr-hall-of-helix/presence/telegram.yaml # the declared presence (campaign owns)
fluid-telegram/presence/telegram.schema.yaml # the schema it validates against
fluid-telegram/presence/resolved/<campaign>.yaml # what provisioning produced
```
The provisioner takes a spec path as its input, so this interface stays reusable
by any HelixForge campaign rather than being about the Hall of Helix. It follows
the split `FT-WP-0001` T13 already draws: what to say belongs to the campaign;
how it is carried and proven belongs here.
## The two planes
```
provisioning plane request path
------------------ ------------
operator (human) triggers pr-hall-of-helix
| |
v v POST /v1/channel-posts
provision plan ──▶ human reads ──▶ apply fluid-core ──▶ adapter ──▶ Bot API
| (post_messages only)
v
MTProto as operator account
BotFather · channels.createChannel · channels.editAdmin
```
They share nothing but OpenBao and the resolved-state file. **The adapter has no
provisioning code path and no MTProto session.** It holds a bot token with
`post_messages` and nothing else, so a compromised adapter cannot create,
rename, delete or re-permission anything.
This is what keeps the scheme inside `InterfaceEvolutionIntent.md` §7, which
forbids the Daimon from creating channels or bots or changing permissions. The
provisioner is not the Daimon: it is operator-triggered, it is outside the
request path, and every run is a plan a person approved. §7's prohibition is on
*autonomous* administration, and nothing here is autonomous.
## plan / apply
Terraform's shape, deliberately, because the semantics are already understood.
- **`plan`** reads the spec, reads live Telegram state through MTProto, reads the
resolved-state file, and prints a diff. It writes nothing.
- **`apply`** executes a plan the operator approved. It is idempotent: a second
apply over an unchanged spec is a no-op, and every action is keyed on a
resolved id rather than on a name.
Reconciliation is **converging, never destructive**:
- The provisioner has **no destroy path.** Deleting a channel destroys its
subscribers and its post history irreversibly, and no specification is worth
trusting with that. Removing a channel from the spec produces a warning, not a
deletion.
- **Drift on a destructive field reports and stops.** If a channel's username has
been taken over, or the bot has been demoted, `apply` says so and exits
non-zero. It does not "fix" its way through a situation it does not understand.
- **Administrator rights are clamped, not declared.** The provisioner grants
`post_messages` and refuses to grant anything else *even if the spec asks for
it*. A spec requesting `can_delete_messages` fails validation. The intent
forbids the system from holding a right it is not allowed to exercise, so the
safe place to enforce that is where the right is granted.
- **The private test channel is created before the public one**, and `apply`
refuses to touch the public channel until the private one has recorded a
successful test publication. Canon PUB-01, and `FT-WP-0001` T02.
## Secrets
Nothing sensitive enters the spec or the resolved state. Both are committed.
| Secret | Path | Rule |
|---|---|---|
| MTProto session string | `bao:.../telegram/operator-session` | Provisioning only. Never given to the adapter. |
| `api_id` / `api_hash` | `bao:.../telegram/operator-app` | Provisioning only. |
| Bot token | `bao:.../telegram/bot-token` | Written by `apply` the moment BotFather issues it, read by the adapter at start. Never logged, never echoed, not even in error text. |
| Redaction salt | `bao:.../telegram/redaction-salt` | **Create-if-absent, never overwrite.** |
The salt rule is a hard one. `docs/observation.md` explains why: rotating it
silently invalidates every longitudinal comparison the interface has ever made,
and it does so without any visible failure. So the provisioner generates it once
if it is missing and will not replace it — the only way to change it is a
deliberate human write to OpenBao, which is a decision, not a run.
## What provisioning produces
`presence/resolved/<campaign>.yaml`, committed, no secrets:
```yaml
resolved:
campaign: hall-of-helix
spec_digest: "sha256:..." # the spec this state came from
provisioned_at: "..."
bot:
username: "..."
id: 123456789
channels:
test: { chat_id: -100..., username: null, admin_rights: [post_messages] }
public: { chat_id: -100..., username: "...", admin_rights: [post_messages] }
```
This is what the adapter's configuration is derived from, and what `plan` diffs
against. `spec_digest` is the link that makes drift detectable: if the spec
changed and the digest did not, provisioning is behind.
## Rate and account safety
Automating a user account is not what Telegram's terms are written around. Three
mitigations, all cheap:
- Use a **dedicated operator account**, never a person's own. If it is limited,
nothing personal is lost.
- The provisioner runs at **human pace** — it is a reconciler that runs on
change, not a loop. BotFather conversations are rate-limited to one exchange at
a time with real delays between messages.
- The session **publishes nothing.** All ordinary traffic goes through the bot
token on the Bot API, which is the supported surface. The user session exists
only to do the three things the Bot API cannot.
## Open question for the operator
`InterfaceEvolutionIntent.md` §7 lists channel and bot creation under "explicit
non-authority" without distinguishing the Daimon from an operator-run tool. The
reading above — that §7 constrains autonomous action, and an approved plan is not
autonomous — is the intended one, but it is currently a reading rather than
something the document says. A one-sentence clarification in §7 would settle it.
That is a governance edit to a constitutional document, so it is proposed here
rather than made.

View file

@ -0,0 +1,44 @@
# Worked example of a declared presence.
#
# The real instance belongs in pr-hall-of-helix/presence/telegram.yaml — this
# copy exists so the schema has something to be read against, and so the
# provisioner's tests have a fixture. Keep it in sync with the schema, not with
# the campaign.
presence:
schema_version: "0.1"
campaign: "hall-of-helix"
interface: "helix-forge-telegram-publishing"
bot:
name: "HelixForge"
username_preference:
- "HelixForgeBot"
- "HelixForgePublishBot"
- "HelixForgeHallBot"
about: >-
Notes from the Hall of Helix — the people and sessions building HelixForge.
description: >-
HelixForge turns intent into structure, structure into capability, and
capability into lasting progress. This channel carries short accounts of
the work and the people doing it, each linking back to the entry it was
written from.
avatar: "presence/assets/helixforge-avatar.png"
channels:
test:
title: "HelixForge — test"
description: "Private rendering checks. Nothing here is published."
visibility: private
admin_rights: [post_messages]
public:
title: "Hall of Helix"
description: >-
Short notes on the work going into HelixForge, and the people doing it.
visibility: public
username_preference:
- "hallofhelix"
- "halloftheHelix"
admin_rights: [post_messages]
linked_discussion_group: false

View file

@ -0,0 +1,114 @@
# Declared presence — schema for a campaign's Telegram surface.
#
# The instance lives in the campaign repo (pr-hall-of-helix/presence/telegram.yaml).
# This file is the contract it is validated against, and lives here because the
# provisioner is here. See docs/provisioning.md.
$schema: "https://json-schema.org/draft/2020-12/schema"
$id: "fluid-telegram/presence/telegram.schema.yaml"
title: "Telegram declared presence"
type: object
required: [presence]
additionalProperties: false
properties:
presence:
type: object
required: [schema_version, campaign, interface, bot, channels]
additionalProperties: false
properties:
schema_version: { const: "0.1" }
campaign:
type: string
description: >
Campaign slug. Names the resolved-state file and the OpenBao subtree,
so it must be stable for the life of the presence.
pattern: "^[a-z0-9][a-z0-9-]*$"
interface:
type: string
description: The FLUID interface that will publish through this presence.
const: "helix-forge-telegram-publishing"
bot:
type: object
required: [name, about, description]
additionalProperties: false
properties:
name:
type: string
maxLength: 64
description: Display name. What a reader encountering it cold sees first.
username_preference:
type: array
description: >
Candidate usernames in order. BotFather requires a globally unique
name ending in "bot" or "_bot", and the first choice is often taken.
Declaring fallbacks keeps apply from stopping on a collision; the
one actually issued is recorded in the resolved state.
items: { type: string, pattern: "^[A-Za-z][A-Za-z0-9_]{3,30}([Bb]ot|_bot)$" }
minItems: 1
about:
type: string
maxLength: 120
description: BotFather /setabouttext — shown on the profile before a chat starts.
description:
type: string
maxLength: 512
description: BotFather /setdescription — shown in the empty chat.
avatar:
type: string
description: >
Repo-relative path to a square image in the campaign repo.
Provisioning is content-addressed on its digest, so replacing the
file is what triggers an update.
channels:
type: object
required: [test, public]
additionalProperties: false
description: >
Exactly two. The test channel is created first and is where every
verification runs; nothing reaches the public channel until a person
has looked at a rendering in the test channel (Canon PUB-01).
properties:
test: { $ref: "#/$defs/channel" }
public: { $ref: "#/$defs/channel" }
linked_discussion_group:
type: boolean
default: false
description: >
Whether the public channel has a linked group for comments. Off by
default: comments are an inbound surface, and this interface is
outbound only. Turning it on is a scope change, not a setting.
$defs:
channel:
type: object
required: [title, visibility]
additionalProperties: false
properties:
title:
type: string
maxLength: 128
description:
type: string
maxLength: 255
visibility:
enum: [private, public]
username_preference:
type: array
description: Public channels only. Ordered candidates; a private channel must not declare one.
items: { type: string, pattern: "^[A-Za-z][A-Za-z0-9_]{4,31}$" }
admin_rights:
type: array
description: >
Rights granted to the bot. The provisioner clamps this: post_messages
is the only permitted value, and any other entry fails validation
rather than being silently dropped. Declared explicitly so that a
reader of the spec can see the boundary rather than infer it.
items: { const: post_messages }
default: [post_messages]
maxItems: 1

View file

@ -43,3 +43,4 @@ fluid_pressure:
linked_hypotheses:
- "H-tg-visual"
- "H-tg-textonly"

View file

@ -27,8 +27,8 @@ revision:
# failing fast would turn a rate limit into a lost entry.
timeout_ms: 30000
retry:
# Only GET is retried by the connector; POST /v1/hall-entries is
# idempotent on entry_id, but the retry decision stays with the adapter,
# Only GET is retried by the connector. POST /v1/channel-posts is
# idempotent on post_id, but the retry decision stays with the adapter,
# which knows whether Telegram accepted the message.
max_attempts: 1
circuit_breaker:

View file

@ -46,6 +46,14 @@ first experiment. See `docs/` and the handover package at
What remains needs either a person with a Telegram account or a running
adapter.
**T01T03 have moved.** They were written as manual steps; they are now
provisioned from a declared specification by `FT-WP-0002`, and the three tasks
below are kept as the record of what must be true rather than as instructions to
follow. The Bot API cannot create a bot or a channel, so provisioning is an
MTProto client acting as a designated operator account (Canon INT-03); one
bounded human bootstrap remains, and `docs/provisioning.md` says exactly what it
is and why it cannot be removed.
## T01 — Register the bot
```task
@ -55,14 +63,18 @@ priority: high
state_hub_task_id: "a5449e86-3181-54b6-a0fe-e5c761b3e4ba"
```
**Human step.** Register the HelixForge bot through BotFather (Canon BOT-01).
Bots are first-class Telegram accounts and need no phone number.
**Superseded by `FT-WP-0002` T04.** The bot is registered by the provisioner
from the declared presence spec, not by hand: BotFather `/newbot` driven over
MTProto, name, about text, description and avatar set from the spec, and the
token written straight to OpenBao without passing through a terminal. Consistent
with `HF-WP-0002` (bao.coulomb.social).
Set the bot's name, description and profile image to something a reader
encountering it cold would recognise as HelixForge.
What survives from the original step is the requirement, not the method: a
reader encountering the bot cold should recognise it as HelixForge. That is now
`bot.name` / `bot.about` / `bot.description` in the spec, where it is reviewable
before it is applied.
Write the token to OpenBao immediately; do not paste it anywhere else, not even
briefly. Consistent with `HF-WP-0002` (bao.coulomb.social).
Close this task when `FT-WP-0002` T04 has run. See `docs/provisioning.md`.
## T02 — Create the channels
@ -73,13 +85,21 @@ priority: high
state_hub_task_id: "597ab1b7-5903-5a5d-a510-0a6946325404"
```
**Human step.** Create the **private test channel first**, then the public
channel (Canon PUB-01). Add the bot as administrator with **Post Messages**
only — the intent forbids it from using any wider right, so granting one would
be a permission the system is not allowed to exercise.
**Superseded by `FT-WP-0002` T04.** Both channels are created by the
provisioner via `channels.createChannel`, private test channel first (Canon
PUB-01), with the bot added as administrator through `channels.editAdmin`.
Every verification below runs against the private channel. Nothing reaches the
public channel until the rendering has been seen by a person.
The ordering and the rights are no longer conventions a person is trusted to
follow. `apply` will not touch the public channel until the test channel has
recorded a successful publication, and it grants **Post Messages** and refuses
anything wider *even if the spec asks for it* — the intent forbids the system
from holding a right it may not exercise, so the grant is the right place to
enforce that.
Every verification below still runs against the private channel, and nothing
reaches the public channel until a person has looked at a rendering.
Close this task when `FT-WP-0002` T04 has run.
## T03 — Generate and store the redaction salt
@ -90,9 +110,15 @@ priority: high
state_hub_task_id: "bbe5da73-e604-5dfe-b25f-fba802f67618"
```
`openssl rand -hex 32`, stored in OpenBao beside the token, injected as
`FLUID_REDACTION_SALT`. It must never change: see `docs/observation.md` for why
rotating it silently invalidates every longitudinal comparison.
**Superseded by `FT-WP-0002` T05.** The provisioner generates 32 bytes if and
only if the OpenBao path is empty, and has no path that overwrites one. Injected
as `FLUID_REDACTION_SALT`.
It must never change: see `docs/observation.md` for why rotating it silently
invalidates every longitudinal comparison. Making that a property of the tool
rather than a note in a workplan is the whole reason the step moved.
Close this task when `FT-WP-0002` T05 has run.
## T04 — Record the intent and publish R-1

View file

@ -0,0 +1,178 @@
---
id: FT-WP-0002
type: workplan
title: "Provision the Telegram presence from a declared specification"
domain: infotech
repo: fluid-telegram
status: proposed
owner: worsch
topic_slug: fluid-telegram
created: "2026-09-04"
updated: "2026-09-04"
planning_priority: high
planning_order: 2
depends_on:
- FT-WP-0001
related_repos:
- pr-hall-of-helix
- helix-forge
---
# FT-WP-0002 — Declared presence provisioning
Replace the manual route of `FT-WP-0001` T01T03 with a specification and a
reconciler. The design, its constraints and its guardrails are in
`docs/provisioning.md`; this workplan implements it.
The shape of the problem, stated once: the Bot API cannot create a bot or a
channel. Both are client capabilities reachable only through MTProto with a user
account (Canon INT-03). So provisioning is an MTProto client acting as a
designated operator, and three things stay human — a phone number, the
`api_id`/`api_hash` web form, and the login code that mints the session. That is
one bounded bootstrap, after which the surface is declared and converges.
## T01 — Bootstrap the operator account and session
```task
id: FT-WP-0002-T01
status: todo
priority: high
```
**The irreducible human step.** Register a dedicated Telegram account for the
operator — never a person's own, so that a rate limit costs nothing personal.
Obtain `api_id` / `api_hash` from my.telegram.org.
Then `provision session bootstrap` runs the interactive login once and writes the
session string to OpenBao. The command is the only interactive one in the tool,
and it exists so that the interactive part is bounded and named rather than
spread through the process.
Nothing else in this workplan is interactive.
## T02 — Publish the presence schema and the campaign's spec
```task
id: FT-WP-0002-T02
status: todo
priority: high
```
The schema is in `presence/telegram.schema.yaml` with a worked example beside it.
What remains is the real instance at `pr-hall-of-helix/presence/telegram.yaml`,
plus the avatar asset.
The spec is editorial — a bot's name, its description, the channel titles a
stranger reads — so it belongs to the campaign. The provisioner takes a spec path
as input, which is what keeps this interface reusable by campaigns other than the
Hall of Helix.
Blocked on `pr-hall-of-helix` existing.
## T03 — Implement `provision plan`
```task
id: FT-WP-0002-T03
status: todo
priority: high
```
Read the spec, validate it against the schema, read live state through MTProto,
read `presence/resolved/<campaign>.yaml`, print a diff. Write nothing.
Validation is where the rights clamp lives: an `admin_rights` entry other than
`post_messages` fails here, loudly, rather than being quietly dropped. The intent
forbids the system from holding a right it may not exercise, and a spec is the
cheapest place to catch someone asking for one.
`plan` must be honest about what it cannot see. A username's availability is not
knowable without attempting it, so the plan says "will attempt, with fallbacks"
rather than promising an outcome.
## T04 — Implement `provision apply`
```task
id: FT-WP-0002-T04
status: todo
priority: high
```
Execute an approved plan. Idempotent, keyed on resolved ids rather than names.
- Bot: BotFather `/newbot`, then `/setabouttext`, `/setdescription`, `/setuserpic`.
Parse the token from the reply, write it straight to OpenBao, and hold it in
memory nowhere longer than that call. Walk `username_preference` on collision.
- Channels: `channels.createChannel`, test channel first. Set the public
username; a private channel that declares one is a validation error, not a
silent skip.
- Bot as administrator: `channels.editAdmin` with `post_messages` and nothing
else.
- Write `presence/resolved/<campaign>.yaml` with the spec digest.
Three refusals carry the weight, and each is a real failure mode rather than a
hypothetical: there is **no destroy path** — removing a channel from the spec
warns, because deleting one destroys its subscribers and history irreversibly;
**drift on a destructive field stops the run** — a demoted bot or a taken-over
username exits non-zero rather than being "fixed" by a tool that does not
understand what happened; and **the public channel is not touched** until the
test channel has recorded a successful publication.
## T05 — Provision the redaction salt
```task
id: FT-WP-0002-T05
status: todo
priority: high
```
Generate 32 bytes if and only if the OpenBao path is empty, and never overwrite.
Supersedes `FT-WP-0001` T03.
The create-if-absent rule is not defensiveness. `docs/observation.md` explains
that rotating the salt silently invalidates every longitudinal comparison the
interface has made, and does so with no visible failure — the numbers keep
arriving and quietly stop meaning what they used to. A tool that can rewrite it
is a tool that will eventually rewrite it, so the only path to a new salt is a
deliberate human write, which is a decision rather than a run.
## T06 — Derive adapter configuration from the resolved state
```task
id: FT-WP-0002-T06
status: todo
priority: medium
```
The adapter reads its chat id and bot token from the resolved state and OpenBao.
It gets no MTProto session and no provisioning code path, so a compromised
adapter cannot create, rename, delete or re-permission anything. That separation
is the point of running two planes rather than one tool with a flag.
## T07 — Drift check
```task
id: FT-WP-0002-T07
status: todo
priority: low
```
`provision plan --check` exits non-zero on drift, suitable for a scheduled run.
Report only; never auto-apply. An unattended reconciler that can change a public
channel is the thing IEI-1 §7 exists to prevent.
## T08 — Settle the §7 reading
```task
id: FT-WP-0002-T08
status: todo
priority: medium
```
`InterfaceEvolutionIntent.md` §7 lists channel and bot creation under explicit
non-authority without distinguishing the Daimon from an operator-run tool. The
intended reading is that §7 constrains *autonomous* action and an approved plan
is not autonomous — but that is currently a reading rather than something the
document says, and a constitutional document should not depend on one.
Add a one-sentence clarification, as a governance change under §20, before T04
runs against anything real.