session check validated Coulomb Social. FT-WP-0002 is finished. Live apply is still outstanding. Assistant: grok Assistant-Session: 01a10938-ab49-7280-877a-c06f64c06ddd
252 lines
11 KiB
Markdown
252 lines
11 KiB
Markdown
---
|
||
id: FT-WP-0002
|
||
type: workplan
|
||
title: "Provision the Telegram presence from a declared specification"
|
||
domain: infotech
|
||
repo: fluid-telegram
|
||
status: finished
|
||
flavor: implementation
|
||
owner: worsch
|
||
topic_slug: fluid-telegram
|
||
created: "2026-09-04"
|
||
updated: "2026-10-07"
|
||
planning_priority: high
|
||
planning_order: 2
|
||
depends_on: []
|
||
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.
|
||
|
||
**Finished (2026-10-07).** T02–T08 were already done. The founder bootstrapped
|
||
the dedicated operator session. `session check` returned Coulomb Social, id
|
||
8934269972, and preflight now finds the session present. The credential lane
|
||
request remains `docs/requests/ops-warden-telegram-operator-lane.md`. Live
|
||
`provision apply` is the next act and belongs to the accepted bot and channel
|
||
reviews, not to another task in this plan.
|
||
|
||
## T01 — Write the seeding runbook and bootstrap the session
|
||
|
||
```task
|
||
id: FT-WP-0002-T01
|
||
status: done
|
||
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.
|
||
|
||
**Done (2026-10-07).** The founder created a dedicated account, stored the
|
||
application credentials, and ran `provision session bootstrap --campaign
|
||
hall-of-helix`. `session check` validated Coulomb Social, id 8934269972. The
|
||
session is in OpenBao and was not written to disk.
|
||
|
||
2026-10-03: human gate routed to hub decision 7f455718-66c9-4049-9aaf-1143aea39d34 (D6: Telegram operator session: register the bots, create the channels, bootstrap the user session) by CUST-WP-0075; resolving that decision raises C-40 here.
|
||
|
||
## T02 — Publish the presence schema and the campaign's spec
|
||
|
||
```task
|
||
id: FT-WP-0002-T02
|
||
status: done
|
||
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.
|
||
|
||
**Done.** The avatar is provided at `assets/helix-forge.png` and validated:
|
||
PNG, dimensions, size, and a content digest so replacing the file is what
|
||
triggers an update.
|
||
|
||
## T03 — Implement `provision plan`
|
||
|
||
```task
|
||
id: FT-WP-0002-T03
|
||
status: done
|
||
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.
|
||
|
||
**Done.** Go, `cmd/provision`, with `internal/spec`, `internal/state` and
|
||
`internal/plan`. Live observation is behind `plan.Live`; `internal/tg.Live`
|
||
implements it over MTProto. `--offline` still exists for a plan that does not
|
||
contact Telegram, and says so rather than presenting an unobserved plan as a
|
||
drift check.
|
||
|
||
A spec digest change is an `update` of `spec`. BotFather profile fields the tool
|
||
cannot observe are a `reassert`: apply will repeat them, `plan --check` will not
|
||
treat them as drift.
|
||
|
||
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
|
||
distinct kinds: a deferral holds one action, a block stops the run.
|
||
|
||
## T04 — Implement `provision apply`
|
||
|
||
```task
|
||
id: FT-WP-0002-T04
|
||
status: done
|
||
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.
|
||
|
||
**Done as implementation.** `internal/apply` executes the plan against a
|
||
`Surface` (live MTProto, or a fake in tests). It reasserts the bot profile on
|
||
every apply, updates channel title/description when the spec digest changes, and
|
||
writes the token to OpenBao before anything else can fail. The first live apply
|
||
still waits on T01's session.
|
||
|
||
## 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: done
|
||
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.
|
||
|
||
**Done.** `internal/adaptercfg` maps the resolved state onto one channel's chat
|
||
id and the OpenBao ref of the bot token. `provision adapter-config` prints that
|
||
and never the token. `LoadToken` reads `bot-token` only; tests fail if the
|
||
operator session is requested.
|
||
|
||
## T07 — Drift check
|
||
|
||
```task
|
||
id: FT-WP-0002-T07
|
||
status: done
|
||
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-2 §7 exists to prevent.
|
||
|
||
**Done.** `--check` uses `Plan.Drifted()`, which ignores warnings, deferrals and
|
||
profile reassertion. A changed spec digest is drift; a converged presence is
|
||
quiet. `--check` cannot be combined with `--offline`.
|
||
|
||
## T08 — Settle the §7 reading
|
||
|
||
```task
|
||
id: FT-WP-0002-T08
|
||
status: done
|
||
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 was a reading rather than something the document
|
||
said, and a constitutional document should not depend on one.
|
||
|
||
**Done.** IEI-2 adds: "These prohibitions bind the Daimon acting autonomously.
|
||
An operator-triggered provisioning tool that executes a plan a person has
|
||
approved is not the Daimon and is not autonomous: it is how the presence is
|
||
established, not a grant of authority to the request path." Recorded as a
|
||
governance amendment under §20. The request-path prohibitions are unchanged.
|