fluid-telegram/workplans/FT-WP-0002-declared-presence-provisioning.md
tegwick f06be1f949 Add the operator seeding runbook and enforce the presence schema
Loosen the all-or-nothing stance on provisioning: where a platform has no
API to seed access, a guided runbook is the right answer. docs/seeding-runbook.md
covers the three Telegram steps that cannot be automated, and each carries a
"why not automated" line so the judgement can be revisited rather than
inherited. Telegram's steps resist automation incidentally -- nobody built
the endpoints -- unlike a control such as KYC, which resists by design and
where a weak component would be a defect rather than an opening.

Fix the schema's $id, which was a relative path and broke $ref resolution in
ordinary validators, and add the visibility/username constraint the field
descriptions already claimed. Both specs now validate, and the rejections are
tested.

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
2026-09-04 19:30:54 +02:00

204 lines
7.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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` 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 — Write the seeding runbook and bootstrap the session
```task
id: FT-WP-0002-T01
status: todo
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: todo
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.
## T04 — Implement `provision apply`
```task
id: FT-WP-0002-T04
status: todo
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: todo
priority: high
state_hub_task_id: "d5fd905a-6abd-5ae1-9105-abc4c2a7127e"
```
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
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: todo
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.