166 lines
7.8 KiB
Markdown
166 lines
7.8 KiB
Markdown
|
|
# Declared presence — provisioning the Telegram surface
|
|||
|
|
|
|||
|
|
`FT-WP-0001` T01–T03 were written as human steps: register a bot through
|
|||
|
|
BotFather, create two channels, generate a salt. This document replaces that
|
|||
|
|
route with a declared specification and a reconciler, and states honestly which
|
|||
|
|
part of it cannot be automated and why.
|
|||
|
|
|
|||
|
|
## The constraint that shapes everything
|
|||
|
|
|
|||
|
|
**The Bot API cannot create a bot or a channel.** It can only act as one. Both
|
|||
|
|
acts are client capabilities, reachable only through MTProto with a *user*
|
|||
|
|
account — Canon INT-03, "capabilities unavailable through the Bot API".
|
|||
|
|
|
|||
|
|
So the provisioner is an MTProto client acting as a designated operator account.
|
|||
|
|
It messages BotFather the way a person would, and it calls `channels.createChannel`
|
|||
|
|
the way a client would.
|
|||
|
|
|
|||
|
|
Three things genuinely cannot be derived from a specification:
|
|||
|
|
|
|||
|
|
| Human step | Why | Frequency |
|
|||
|
|
|---|---|---|
|
|||
|
|
| A Telegram account for the operator | Requires a phone number and a device | Once |
|
|||
|
|
| `api_id` / `api_hash` | Issued by a web form at my.telegram.org | Once |
|
|||
|
|
| The login code (and 2FA password) that mints the MTProto session | Telegram sends it out of band, by design | Once, and again if the session is revoked |
|
|||
|
|
|
|||
|
|
Everything else — bot registration, naming, description, avatar, channel
|
|||
|
|
creation, visibility, administrator rights, the redaction salt — is declared and
|
|||
|
|
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.
|
|||
|
|
|
|||
|
|
## Where the specification lives
|
|||
|
|
|
|||
|
|
The presence spec is **editorial**, not mechanical. It carries the bot's name,
|
|||
|
|
its description, its avatar, and the channel titles a stranger will read. That is
|
|||
|
|
brand, and brand belongs to the campaign:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
pr-hall-of-helix/presence/telegram.yaml # the declared presence (campaign owns)
|
|||
|
|
fluid-telegram/presence/telegram.schema.yaml # the schema it validates against
|
|||
|
|
fluid-telegram/presence/resolved/<campaign>.yaml # what provisioning produced
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
The provisioner takes a spec path as its input, so this interface stays reusable
|
|||
|
|
by any HelixForge campaign rather than being about the Hall of Helix. It follows
|
|||
|
|
the split `FT-WP-0001` T13 already draws: what to say belongs to the campaign;
|
|||
|
|
how it is carried and proven belongs here.
|
|||
|
|
|
|||
|
|
## The two planes
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
provisioning plane request path
|
|||
|
|
------------------ ------------
|
|||
|
|
operator (human) triggers pr-hall-of-helix
|
|||
|
|
| |
|
|||
|
|
v v POST /v1/channel-posts
|
|||
|
|
provision plan ──▶ human reads ──▶ apply fluid-core ──▶ adapter ──▶ Bot API
|
|||
|
|
| (post_messages only)
|
|||
|
|
v
|
|||
|
|
MTProto as operator account
|
|||
|
|
BotFather · channels.createChannel · channels.editAdmin
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
They share nothing but OpenBao and the resolved-state file. **The adapter has no
|
|||
|
|
provisioning code path and no MTProto session.** It holds a bot token with
|
|||
|
|
`post_messages` and nothing else, so a compromised adapter cannot create,
|
|||
|
|
rename, delete or re-permission anything.
|
|||
|
|
|
|||
|
|
This is what keeps the scheme inside `InterfaceEvolutionIntent.md` §7, which
|
|||
|
|
forbids the Daimon from creating channels or bots or changing permissions. The
|
|||
|
|
provisioner is not the Daimon: it is operator-triggered, it is outside the
|
|||
|
|
request path, and every run is a plan a person approved. §7's prohibition is on
|
|||
|
|
*autonomous* administration, and nothing here is autonomous.
|
|||
|
|
|
|||
|
|
## plan / apply
|
|||
|
|
|
|||
|
|
Terraform's shape, deliberately, because the semantics are already understood.
|
|||
|
|
|
|||
|
|
- **`plan`** reads the spec, reads live Telegram state through MTProto, reads the
|
|||
|
|
resolved-state file, and prints a diff. It writes nothing.
|
|||
|
|
- **`apply`** executes a plan the operator approved. It is idempotent: a second
|
|||
|
|
apply over an unchanged spec is a no-op, and every action is keyed on a
|
|||
|
|
resolved id rather than on a name.
|
|||
|
|
|
|||
|
|
Reconciliation is **converging, never destructive**:
|
|||
|
|
|
|||
|
|
- The provisioner has **no destroy path.** Deleting a channel destroys its
|
|||
|
|
subscribers and its post history irreversibly, and no specification is worth
|
|||
|
|
trusting with that. Removing a channel from the spec produces a warning, not a
|
|||
|
|
deletion.
|
|||
|
|
- **Drift on a destructive field reports and stops.** If a channel's username has
|
|||
|
|
been taken over, or the bot has been demoted, `apply` says so and exits
|
|||
|
|
non-zero. It does not "fix" its way through a situation it does not understand.
|
|||
|
|
- **Administrator rights are clamped, not declared.** The provisioner grants
|
|||
|
|
`post_messages` and refuses to grant anything else *even if the spec asks for
|
|||
|
|
it*. A spec requesting `can_delete_messages` fails validation. The intent
|
|||
|
|
forbids the system from holding a right it is not allowed to exercise, so the
|
|||
|
|
safe place to enforce that is where the right is granted.
|
|||
|
|
- **The private test channel is created before the public one**, and `apply`
|
|||
|
|
refuses to touch the public channel until the private one has recorded a
|
|||
|
|
successful test publication. Canon PUB-01, and `FT-WP-0001` T02.
|
|||
|
|
|
|||
|
|
## Secrets
|
|||
|
|
|
|||
|
|
Nothing sensitive enters the spec or the resolved state. Both are committed.
|
|||
|
|
|
|||
|
|
| Secret | Path | Rule |
|
|||
|
|
|---|---|---|
|
|||
|
|
| MTProto session string | `bao:.../telegram/operator-session` | Provisioning only. Never given to the adapter. |
|
|||
|
|
| `api_id` / `api_hash` | `bao:.../telegram/operator-app` | Provisioning only. |
|
|||
|
|
| Bot token | `bao:.../telegram/bot-token` | Written by `apply` the moment BotFather issues it, read by the adapter at start. Never logged, never echoed, not even in error text. |
|
|||
|
|
| Redaction salt | `bao:.../telegram/redaction-salt` | **Create-if-absent, never overwrite.** |
|
|||
|
|
|
|||
|
|
The salt rule is a hard one. `docs/observation.md` explains why: rotating it
|
|||
|
|
silently invalidates every longitudinal comparison the interface has ever made,
|
|||
|
|
and it does so without any visible failure. So the provisioner generates it once
|
|||
|
|
if it is missing and will not replace it — the only way to change it is a
|
|||
|
|
deliberate human write to OpenBao, which is a decision, not a run.
|
|||
|
|
|
|||
|
|
## What provisioning produces
|
|||
|
|
|
|||
|
|
`presence/resolved/<campaign>.yaml`, committed, no secrets:
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
resolved:
|
|||
|
|
campaign: hall-of-helix
|
|||
|
|
spec_digest: "sha256:..." # the spec this state came from
|
|||
|
|
provisioned_at: "..."
|
|||
|
|
bot:
|
|||
|
|
username: "..."
|
|||
|
|
id: 123456789
|
|||
|
|
channels:
|
|||
|
|
test: { chat_id: -100..., username: null, admin_rights: [post_messages] }
|
|||
|
|
public: { chat_id: -100..., username: "...", admin_rights: [post_messages] }
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
This is what the adapter's configuration is derived from, and what `plan` diffs
|
|||
|
|
against. `spec_digest` is the link that makes drift detectable: if the spec
|
|||
|
|
changed and the digest did not, provisioning is behind.
|
|||
|
|
|
|||
|
|
## Rate and account safety
|
|||
|
|
|
|||
|
|
Automating a user account is not what Telegram's terms are written around. Three
|
|||
|
|
mitigations, all cheap:
|
|||
|
|
|
|||
|
|
- Use a **dedicated operator account**, never a person's own. If it is limited,
|
|||
|
|
nothing personal is lost.
|
|||
|
|
- The provisioner runs at **human pace** — it is a reconciler that runs on
|
|||
|
|
change, not a loop. BotFather conversations are rate-limited to one exchange at
|
|||
|
|
a time with real delays between messages.
|
|||
|
|
- The session **publishes nothing.** All ordinary traffic goes through the bot
|
|||
|
|
token on the Bot API, which is the supported surface. The user session exists
|
|||
|
|
only to do the three things the Bot API cannot.
|
|||
|
|
|
|||
|
|
## Open question for the operator
|
|||
|
|
|
|||
|
|
`InterfaceEvolutionIntent.md` §7 lists channel and bot creation under "explicit
|
|||
|
|
non-authority" without distinguishing the Daimon from an operator-run tool. The
|
|||
|
|
reading above — that §7 constrains autonomous action, and an approved plan is not
|
|||
|
|
autonomous — is the intended one, but it is currently a reading rather than
|
|||
|
|
something the document says. A one-sentence clarification in §7 would settle it.
|
|||
|
|
That is a governance edit to a constitutional document, so it is proposed here
|
|||
|
|
rather than made.
|