fluid-telegram/workplans/FT-WP-0002-declared-presence-provisioning.md
tegwick 9defd362c6 Record the stored Hall of Helix operator session.
session check validated Coulomb Social. FT-WP-0002 is finished. Live apply is still outstanding.

Assistant: grok
Assistant-Session: 01a10938-ab49-7280-877a-c06f64c06ddd
2026-10-08 00:49:20 +02:00

11 KiB
Raw Permalink Blame History

id type title domain repo status flavor owner topic_slug created updated planning_priority planning_order depends_on related_repos state_hub_workstream_id
FT-WP-0002 workplan Provision the Telegram presence from a declared specification infotech fluid-telegram finished implementation worsch fluid-telegram 2026-09-04 2026-10-07 high 2
pr-hall-of-helix
helix-forge
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

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

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

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

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

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

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

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

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.