Completes the provisioner's write path. internal/tg drives BotFather as a conversation rather than pretending it is an endpoint, creates channels, claims usernames with fallbacks, and grants post_messages. internal/apply sequences it: bot before administrator, test channel before public, and state saved after every step that changed the world -- a channel that exists but is unrecorded is worse than one that does not exist, because the next run creates a second. The operator session lives in OpenBao, not on disk. gotd's FileStorage would leave a full-account credential in the working directory, where it outlives the run and can be committed by accident. The bot token goes straight from BotFather's reply to OpenBao and is cleared from memory; if that write fails the error says how to recover by hand and warns against re-running, since a retry creates a second bot. Closes T05: the redaction salt is create-if-absent with no overwrite path, and the test asserts it, because rotating it invalidates every longitudinal comparison with no visible failure. 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
219 lines
8.8 KiB
Markdown
219 lines
8.8 KiB
Markdown
---
|
||
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
|
||
state_hub_workstream_id: "f2373858-c932-5a4d-81b6-db9a3595135a"
|
||
---
|
||
|
||
# FT-WP-0002 — Declared presence provisioning
|
||
|
||
Replace the manual route of `FT-WP-0001` T01–T03 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 — Write the seeding runbook and bootstrap the session
|
||
|
||
```task
|
||
id: FT-WP-0002-T01
|
||
status: progress
|
||
priority: high
|
||
state_hub_task_id: "039e5358-07c1-5b01-a186-5ece9aab75e4"
|
||
```
|
||
|
||
**The seeding steps, done by a guided runbook.** `docs/seeding-runbook.md` is
|
||
the deliverable: register a dedicated operator account (never a person's own, so
|
||
a rate limit costs nothing personal), obtain `api_id` / `api_hash`, and run
|
||
`provision session bootstrap` to mint the MTProto session into OpenBao.
|
||
|
||
Manual is acceptable here because it is guided, repeatable and bounded — an
|
||
operator who has never done it follows it once and gets the same result as
|
||
anyone else. What makes it acceptable is the structure, not the shortness.
|
||
|
||
Each step in the runbook carries a **Why not automated** line, and that is the
|
||
part with a future. Telegram's three steps resist automation only incidentally —
|
||
nobody built the endpoints — as opposed to a control like KYC, which resists
|
||
automation by design and where a component weak enough to allow it would be a
|
||
defect rather than an opening. So these lines are re-read when the platform
|
||
changes, and steps are deleted when a reason expires.
|
||
|
||
`provision session bootstrap` is the only interactive command in the tool. Nothing
|
||
else in this workplan is interactive.
|
||
|
||
## T02 — Publish the presence schema and the campaign's spec
|
||
|
||
```task
|
||
id: FT-WP-0002-T02
|
||
status: progress
|
||
priority: high
|
||
state_hub_task_id: "01684264-37a5-5fe8-a491-37686e094428"
|
||
```
|
||
|
||
**Largely done.** The schema is in `presence/telegram.schema.yaml` with a worked
|
||
example beside it, and the real instance is at
|
||
`pr-hall-of-helix/presence/telegram.yaml`. Both validate.
|
||
|
||
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.
|
||
|
||
The schema enforces rather than describes: an `admin_rights` entry other than
|
||
`post_messages` is rejected, a private channel may not declare a username, a
|
||
public one must, and unknown fields fail instead of being ignored. A silently
|
||
dropped field is how a spec stops describing what actually exists.
|
||
|
||
**Remaining:** the avatar asset. `presence/assets/helixforge-avatar.png` is
|
||
referenced but absent, and `apply` will fail on `/setuserpic` until it exists.
|
||
It is left for a person on purpose — it is the first thing a stranger sees of
|
||
HelixForge on Telegram, and a generated placeholder would quietly become
|
||
permanent.
|
||
|
||
## T03 — Implement `provision plan`
|
||
|
||
```task
|
||
id: FT-WP-0002-T03
|
||
status: progress
|
||
priority: high
|
||
state_hub_task_id: "c3923422-7f2e-5bf9-906c-6d59572b09c0"
|
||
```
|
||
|
||
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.
|
||
|
||
**Implemented (2026-09-04), less live observation.** Go, `cmd/provision`, with
|
||
`internal/spec`, `internal/state` and `internal/plan`. Runs against the real
|
||
campaign spec today; live observation is behind a `plan.Live` interface that
|
||
returns an offline stub until T01 mints a session, and the command says so rather
|
||
than presenting an unobserved plan as a drift check.
|
||
|
||
One thing the design got wrong and the implementation surfaced: refusals are two
|
||
different things. A **deferral** is the design working — the public channel
|
||
waiting on a checked rendering, which is normal on every first run. A **block**
|
||
is the world disagreeing with the state file — drifted rights, a taken-over
|
||
username, an unreachable bot. Collapsing them meant a first run could never apply
|
||
anything, since it always defers the public channel. They are now distinct
|
||
kinds: a deferral holds one action, a block stops the run.
|
||
|
||
## T04 — Implement `provision apply`
|
||
|
||
```task
|
||
id: FT-WP-0002-T04
|
||
status: progress
|
||
priority: high
|
||
state_hub_task_id: "5659b72a-8309-58b6-96e9-79137ce41ed4"
|
||
```
|
||
|
||
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: done
|
||
priority: high
|
||
state_hub_task_id: "d5fd905a-6abd-5ae1-9105-abc4c2a7127e"
|
||
```
|
||
|
||
**Implemented.** `secrets.CreateIfAbsent` generates 32 bytes if and only if the
|
||
OpenBao path is empty, and the package has no counterpart that overwrites.
|
||
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
|
||
state_hub_task_id: "c305b83f-b05e-5f09-9182-9896ae137c38"
|
||
```
|
||
|
||
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: progress
|
||
priority: low
|
||
state_hub_task_id: "f3f36415-ed55-5717-b752-706b04ba5458"
|
||
```
|
||
|
||
`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
|
||
state_hub_task_id: "864bcd23-6357-5460-a8e6-98ec9a929e29"
|
||
```
|
||
|
||
`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.
|