diff --git a/docs/provisioning.md b/docs/provisioning.md index b8cf9c4..34149c8 100644 --- a/docs/provisioning.md +++ b/docs/provisioning.md @@ -28,8 +28,20 @@ creation, visibility, administrator rights, the redaction salt — is declared a 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. +bootstrap, then a declared surface that converges.** + +Those steps are not left to improvisation. `docs/seeding-runbook.md` guides them: +what to have ready, what to do, what to record and where, and — for each one — why +it could not be automated. A manual step is acceptable when it is guided, +repeatable and bounded, and unacceptable when it is merely undocumented. + +Worth distinguishing two reasons a step resists automation, because they age +differently. A KYC check resists *by design*; automating it would defeat its +purpose, and a component weak enough to permit it is a defect rather than an +opportunity. A web form that mints an API key resists only *incidentally*, +because nobody built the endpoint. All three Telegram steps are the second kind — +none is a fraud control — so each is a candidate for removal the moment the +platform changes. ## Where the specification lives diff --git a/docs/seeding-runbook.md b/docs/seeding-runbook.md new file mode 100644 index 0000000..36738ff --- /dev/null +++ b/docs/seeding-runbook.md @@ -0,0 +1,149 @@ +# Operator runbook — seeding platform access + +Some platform access cannot be provisioned from a specification. Telegram will +not let software create the *first* credential: an account needs a phone and a +device, and `api_id` / `api_hash` come from a web form behind a login. No API +exists for either, and none is coming, because their purpose is to establish that +a person is on the other end. + +This runbook is what we do instead. It is not a lowering of the bar. **A manual +step is acceptable when it is guided, repeatable, and bounded — and unacceptable +when it is merely undocumented.** The test each step below has to pass is that +an operator who has never done it can follow it once, get the same result as +anyone else, and hand the outcome to a tool that takes over from there. + +## The rule this runbook operates under + +> Automate everything that has an interface. Where a step exists specifically to +> prove a human is present, guide it — and record why it resisted automation, so +> the judgement can be revisited rather than inherited. + +Every manual step below carries a **Why not automated** line. That line is the +thing to re-read when the platform changes. Some of them will stop being true; +the sooner that is noticed, the smaller the runbook gets. + +There is a real distinction here worth keeping. A KYC identity check resists +automation *by design* — automating it would defeat what it is for, and a +component weak enough to allow it is a defect, not an opportunity. A web form +that mints an API key resists automation only *incidentally*, because nobody +built the endpoint. The first stays manual on principle. The second is a +candidate the moment anything changes. + +Telegram's steps are all the second kind. None of them is a fraud control; they +are just interfaces that were never built. + +--- + +## Before you start + +Have these ready. Stopping halfway through to find one is how the session +becomes irreproducible. + +- [ ] A phone number that can receive SMS, **not** a personal one +- [ ] A device with Telegram installed, to complete the first login +- [ ] Access to OpenBao (`bao.coulomb.social`) with write on the interface's path +- [ ] The campaign's presence spec merged (`pr-hall-of-helix/presence/telegram.yaml`) +- [ ] The avatar asset committed and referenced by the spec + +Roughly 20 minutes, once. + +--- + +## Step 1 — Register the operator account + +**Why not automated:** Telegram requires a phone number and an SMS round trip. +There is no registration API at any tier. This is the step whose whole purpose +is to prove a person is present. + +1. Install Telegram on the device and register with the operator phone number. +2. Set a display name that reads as an operator account, not a person. +3. Enable two-factor authentication and put the password in OpenBao at + `/telegram/operator-2fa` immediately. + +**Do not use a personal account.** If it is rate-limited or restricted — which +automation can cause — the loss should be a throwaway account, not someone's +messages. + +**Record:** the phone number, in OpenBao at `/telegram/operator-phone`. + +--- + +## Step 2 — Obtain `api_id` and `api_hash` + +**Why not automated:** issued by a web form at my.telegram.org behind a login +code. No API. This one is incidental rather than principled — if Telegram ever +exposes it, Step 2 disappears. + +1. Go to , log in with the operator number. +2. **API development tools** → create an application. + - App title: `helixforge-provisioner` + - Platform: Other +3. Copy `api_id` and `api_hash`. + +**Record:** both, in OpenBao at `/telegram/operator-app`. + +They are shown once per application. Losing them means creating another. + +--- + +## Step 3 — Mint the MTProto session + +**Why not automated:** the login code arrives out of band, on the device. The +command is interactive by necessity — but *only this command*, and only once. + +```bash +provision session bootstrap --campaign hall-of-helix +``` + +It prompts for the phone number, the login code, and the 2FA password, then +writes the session string to OpenBao at +`/telegram/operator-session`. + +The session string is a **full-account credential** — it can do anything the +account can. It never leaves OpenBao, is never given to the adapter, and is used +only by the provisioner. Treat losing it as losing the account. + +**Verify:** + +```bash +provision session check --campaign hall-of-helix # prints the account, no secrets +``` + +--- + +## Step 4 — Hand over to the provisioner + +From here nothing is manual. The remaining work is `FT-WP-0002` T03–T06: + +```bash +provision plan --spec ../pr-hall-of-helix/presence/telegram.yaml # read the diff +provision apply --spec ../pr-hall-of-helix/presence/telegram.yaml # execute it +``` + +`apply` registers the bot through BotFather, sets its name, about text, +description and avatar, creates both channels, adds the bot as administrator with +`post_messages` only, generates the redaction salt if it is absent, and writes +`presence/resolved/hall-of-helix.yaml`. + +**Read the plan before approving it.** That is the human judgement this design +keeps — not clicking through BotFather, but deciding whether the diff is what +was intended. + +--- + +## Re-running this + +Steps 1–2 are once per operator account, ever. Step 3 repeats only if the session +is revoked — by a password change, an explicit sign-out, or Telegram invalidating +it. When that happens, re-run Step 3 alone; nothing else is affected, because +resolved state and the bot token do not depend on the session that created them. + +Step 4 is safe to repeat at any time. It is a converging reconciler, and a second +apply over an unchanged spec does nothing. + +## When a step stops being necessary + +Re-read the **Why not automated** lines whenever Telegram changes its API surface, +and delete the step if the reason has expired. A runbook nobody prunes becomes a +description of how things used to work, and the manual steps in it start looking +like requirements rather than gaps. diff --git a/presence/telegram.schema.yaml b/presence/telegram.schema.yaml index 555ea3d..c630d6a 100644 --- a/presence/telegram.schema.yaml +++ b/presence/telegram.schema.yaml @@ -4,7 +4,8 @@ # 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" +# No $id: this schema is identified by its path in the repo. A relative $id is +# not a resolvable URI and breaks "$ref" resolution in ordinary validators. title: "Telegram declared presence" type: object required: [presence] @@ -89,6 +90,21 @@ $defs: type: object required: [title, visibility] additionalProperties: false + # A private channel has no username to claim. Declaring one is a mistake + # about what is being created, so it fails here rather than being ignored + # by apply -- a silently dropped field is how a spec stops describing + # what actually exists. + allOf: + - if: + properties: { visibility: { const: private } } + required: [visibility] + then: + not: { required: [username_preference] } + - if: + properties: { visibility: { const: public } } + required: [visibility] + then: + required: [username_preference] properties: title: type: string diff --git a/workplans/FT-WP-0002-declared-presence-provisioning.md b/workplans/FT-WP-0002-declared-presence-provisioning.md index dc02c9e..9f1feea 100644 --- a/workplans/FT-WP-0002-declared-presence-provisioning.md +++ b/workplans/FT-WP-0002-declared-presence-provisioning.md @@ -32,7 +32,7 @@ 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 +## T01 — Write the seeding runbook and bootstrap the session ```task id: FT-WP-0002-T01 @@ -41,36 +41,53 @@ priority: high state_hub_task_id: "039e5358-07c1-5b01-a186-5ece9aab75e4" ``` -**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. +**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. -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. +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. -Nothing else in this workplan is interactive. +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: todo +status: progress priority: high state_hub_task_id: "01684264-37a5-5fe8-a491-37686e094428" ``` -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. +**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. -Blocked on `pr-hall-of-helix` existing. +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`