From 5a785b9d9ff0f3997233a627425582a1a4ad716d Mon Sep 17 00:00:00 2001 From: tegwick Date: Fri, 4 Sep 2026 19:21:08 +0200 Subject: [PATCH] Ground the repo docs and move presence provisioning to a declared spec Align AGENTS.md with the FT-WP- prefix the hub already registered, and rewrite INTENT.md and SCOPE.md, which were still the generated stubs and carried none of the composed-post framing the interface is built around. Fix two artifact inconsistencies: R-1's retry comment still described POST /v1/hall-entries idempotent on entry_id, from before the reframe to composed posts; and the seed pressure record listed only one of the two hypotheses that cite it. Rename it to match its id. Replace the manual T01-T03 route with FT-WP-0002. The Bot API cannot create a bot or a channel, so provisioning is an MTProto client acting as a designated operator account (Canon INT-03), driven from a declared presence spec owned by the campaign. One bounded human bootstrap remains and docs/provisioning.md says why it cannot be removed. Co-Authored-By: Claude Opus 5 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 --- AGENTS.md | 16 +- INTENT.md | 51 ++++- SCOPE.md | 63 +++++-- WORK-RECORDS.md | 24 +++ docs/provisioning.md | 165 ++++++++++++++++ presence/telegram.example.yaml | 44 +++++ presence/telegram.schema.yaml | 114 +++++++++++ ...register-mismatch.yaml => P-register.yaml} | 1 + revisions/R-1.yaml | 4 +- ...0001-telegram-identity-and-hall-channel.md | 56 ++++-- ...-WP-0002-declared-presence-provisioning.md | 178 ++++++++++++++++++ 11 files changed, 670 insertions(+), 46 deletions(-) create mode 100644 WORK-RECORDS.md create mode 100644 docs/provisioning.md create mode 100644 presence/telegram.example.yaml create mode 100644 presence/telegram.schema.yaml rename pressure/{P-register-mismatch.yaml => P-register.yaml} (98%) create mode 100644 workplans/FT-WP-0002-declared-presence-provisioning.md diff --git a/AGENTS.md b/AGENTS.md index 342d057..b52b809 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,7 +7,7 @@ **Domain:** infotech **Repo slug:** fluid-telegram **Topic ID:** `cee7bedf-2b48-46ef-8601-006474f2ad7a` -**Workplan prefix:** `FLUID-WP-` +**Workplan prefix:** `FT-WP-` --- @@ -136,16 +136,16 @@ curl -s -X PATCH "http://127.0.0.1:8000/tasks/" \ Work items originate as files in this repo — not in the hub. The hub is a read/cache/index layer that rebuilds from files. -**File location:** `workplans/FLUID-WP-NNNN-.md` +**File location:** `workplans/FT-WP-NNNN-.md` **Archived location:** finished workplans may move to -`workplans/archived/YYMMDD-FLUID-WP-NNNN-.md`. The `YYMMDD` prefix is +`workplans/archived/YYMMDD-FT-WP-NNNN-.md`. The `YYMMDD` prefix is the completion/archive date; the frontmatter `id` does not change. **Ad Hoc Tasks:** small opportunistic fixes discovered during a session use `workplans/ADHOC-YYYY-MM-DD.md`, workplan id -`FLUID-WP-ADHOC-YYYY-MM-DD`, and task ids -`FLUID-WP-ADHOC-YYYY-MM-DD-T01`, etc. `FLUID-WP` includes its final `-WP` +`FT-WP-ADHOC-YYYY-MM-DD`, and task ids +`FT-WP-ADHOC-YYYY-MM-DD-T01`, etc. `FT-WP` includes its final `-WP` token. Unqualified historic `ADHOC-*` ids are grandfathered and must not be copied into new records. Use this only for low-risk work completed directly; create a normal workplan for anything needing analysis, design, approval, @@ -155,7 +155,7 @@ dependencies, or multiple phases. ```yaml --- -id: FLUID-WP-NNNN +id: FT-WP-NNNN type: workplan title: "..." domain: infotech @@ -183,7 +183,7 @@ API/MCP/frontmatter bridges until `STATE-WP-0069` retires them — see ## Task Title ` ` `task -id: FLUID-WP-NNNN-T01 +id: FT-WP-NNNN-T01 status: wait | todo | progress | done | cancel priority: high | medium | low state_hub_task_id: "" # deterministic UUIDv5; managed by Repo Manager @@ -196,7 +196,7 @@ Status progression: `todo` → `progress` → `done`; use `wait` for waiting/blo **Residuals when finishing:** actionable leftovers become live work records before `status: finished` — usually an intake (`origin: residual`, -`origin_ref: FLUID-WP-NNNN`) or a spawned workplan. Residual is a *role*, +`origin_ref: FT-WP-NNNN`) or a spawned workplan. Residual is a *role*, not a kind. Fleet list lives on State Hub, not in `SCOPE.md`. To create a new workplan: diff --git a/INTENT.md b/INTENT.md index 80e4fa5..3c46915 100644 --- a/INTENT.md +++ b/INTENT.md @@ -7,18 +7,53 @@ updated: "2026-09-04" ## Why it exists -Fluid interface for telegram messaging and interaction platform. +`fluid-telegram` is the FLUID interface `helix-forge-telegram-publishing`: the +deterministic outbound surface through which HelixForge material reaches +Telegram. -Fluid interface for telegram messaging and interaction platform. +It exists because publishing under an organisation's name to an audience of +strangers is a governed act, not a side effect of having a bot token. The +interface makes that act legible: every publication carries a named reviewer, a +recorded consent basis, and a citation to the work it describes, and every one +of them is reconstructible afterwards from the evidence store. + +## What it is not + +**It is not a content system.** The channel does not republish hall entries. A +hall entry is a considered first-person account written for a colleague reading +a repository; a channel post is short, personal and written to travel. The post +is composed *from* the entry and is a different piece of writing. + +Composition therefore happens **before** this interface is called, and belongs +to the campaign repository `pr-hall-of-helix`. The boundary is architectural — +`ArchitectureBlueprint.md` §48.1 forbids an LLM in the mandatory request path — +and editorial, because a post speaks about a named person's work in HelixForge's +voice and deserves a human read before it goes out. + +This repository renders and delivers what it is given. It never writes, selects, +shortens or embellishes. A post that will not fit is refused, not truncated. ## Governing principle -This repository should stay focused on the purpose above. Work that changes its -authority, ownership boundaries, or operational promises should be captured in a -workplan before implementation. +`InterfaceEvolutionIntent.md` (IEI-1) is the constitutional constraint on this +interface and takes precedence over this file where they touch. It is +subordinate in turn to `helix-forge/INTENT.md`. + +Operational authority is **FLUID-2 (Advisory)** by deliberate choice: the Daimon +may observe, classify pressure and propose adaptations, but may not generate +implementations, run experiments unattended, or promote anything. Raising that +mode is a governance change under IEI-1 §20, not an operational decision. + +Work that changes this repository's authority, ownership boundaries, or +operational promises is captured in a workplan before implementation. ## What it enables -- A coding agent can understand why the repository exists before changing it. -- State Hub can register and coordinate work for this repository. -- Future workplans can stay connected to the repository's intended role. +- HelixForge can publish to Telegram under a stated intent, with a contract, a + revision history, and an audit trail that reconstructs any publication. +- A campaign (`pr-hall-of-helix`) can publish through a delivery surface it does + not have to own, and receive per-variant engagement back. +- Delivery questions — attach a portrait or not, which form travels further — + can be settled by experiment rather than by preference. +- The interface is reusable by any HelixForge campaign, not only the Hall of + Helix. diff --git a/SCOPE.md b/SCOPE.md index 6209022..9814043 100644 --- a/SCOPE.md +++ b/SCOPE.md @@ -1,32 +1,69 @@ # SCOPE -> This file was generated by `statehub register`. Refine it as the repository -> boundaries become clearer. - ## One-liner -Fluid interface for telegram messaging and interaction platform. +The deterministic delivery interface that publishes composed HelixForge posts to +Telegram, under a governed contract with a reconstructible audit trail. ## Core Idea -fluid-telegram exists to provide the capability described in INTENT.md. +A caller submits a **composed post** — already written, already reviewed, +carrying a consent basis and a citation to its source entry. This interface +validates it, renders it into the Telegram form the serving revision calls for, +delivers it through the Bot API, and reports how it performed. + +Everything about *what to say and when* lives elsewhere. Everything about *how +it is carried and proven* lives here. ## In Scope -- Maintain the repository's primary implementation. -- Keep docs, tests, and operational metadata current. +- The R-1 contract (`contracts/r1.openapi.yaml`) and its revisions. +- The adapter: rendering, delivery, idempotency on `post_id`, custody of the bot + token for the duration of a call. +- Validation and refusal: no reviewer, no consent basis, or a body that exceeds + the limit for the form being published → refused, never truncated. +- Delivery experiments (`E-tg-delivery`, cohort `telegram-subscribers`) and the + hypotheses behind them. +- Per-variant engagement reporting back to the campaign. +- Observation and redaction policy (`docs/observation.md`). +- Bot and channel configuration, and the provisioning of that configuration from + a declared specification (FT-WP-0002). +- Operational recovery: token rotation, channel administration, revision + rollback (`docs/runbook.md`). ## Out of Scope -- Own unrelated adjacent systems. -- Make irreversible operational decisions without human approval. +- **Composition and editorial voice.** Condensing an entry into a post, choosing + which work is worth publishing, the publishing cadence, variant strategies. + These belong to `pr-hall-of-helix`. +- **Consent records and the review queue.** The campaign obtains and holds them; + this interface only refuses a post that arrives without one. +- **Reading `hall-of-helix`.** The adapter's only knowledge of an entry is the id + and URL a post cites, which it renders as a link. +- **Autonomous administration.** Creating, renaming or deleting channels, groups + or bots, and changing membership or permissions, are forbidden to the Daimon + (IEI-1 §7). Where such acts are automated at all, they run in the separate, + human-approved provisioning plane, never in the request path. +- Owning unrelated adjacent systems. ## Current State -- Status: active; implementation and stability should be verified by the repo agent. +Design-complete, not yet live. Governance, the R-1 contract, the adapter's +connector contract, observation configuration, the seed pressure record and two +competing hypotheses are in place. The bot, the channels and the adapter are +not. + +- Work: `workplans/FT-WP-0001-telegram-identity-and-hall-channel.md` (active) +- Provisioning: `workplans/FT-WP-0002-*` (declared-presence provisioning; supersedes + the manual T01–T03 route) +- Depends on: `fluid-core` `FLUID-WP-0008` (handover) +- Not yet created: `pr-hall-of-helix` ## Getting Oriented -- Start with: INTENT.md -- Agent instructions: AGENTS.md -- Workplans: workplans/ +- Why it exists: `INTENT.md` +- What may change and what may not: `InterfaceEvolutionIntent.md` (IEI-1) +- What the adapter owes: `docs/adapter-contract.md` +- Platform capability reference: `TelegramCapabilityCanon.md` +- Agent instructions: `AGENTS.md` +- Workplans: `workplans/` diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md new file mode 100644 index 0000000..69eb93d --- /dev/null +++ b/WORK-RECORDS.md @@ -0,0 +1,24 @@ +# Work Records — fluid-telegram + +> Generated by `statehub fix-consistency` (CUST-WP-0061-T04, work-record +> stage 3). Do not edit by hand — edit the source file/block listed for +> each record and re-run fix-consistency to refresh this index. Archived +> workplans are omitted; closed decisions/intakes/engagements stay listed +> so recently-resolved work is still visible. [auto] + +| Kind | ID | Status | Lane | Source | +| --- | --- | --- | --- | --- | +| workplan | FT-WP-0001 | active | — | workplans/FT-WP-0001-telegram-identity-and-hall-channel.md | +| task | FT-WP-0001-T01 | todo | — | workplans/FT-WP-0001-telegram-identity-and-hall-channel.md | +| task | FT-WP-0001-T02 | todo | — | workplans/FT-WP-0001-telegram-identity-and-hall-channel.md | +| task | FT-WP-0001-T03 | todo | — | workplans/FT-WP-0001-telegram-identity-and-hall-channel.md | +| task | FT-WP-0001-T04 | todo | — | workplans/FT-WP-0001-telegram-identity-and-hall-channel.md | +| task | FT-WP-0001-T05 | todo | — | workplans/FT-WP-0001-telegram-identity-and-hall-channel.md | +| task | FT-WP-0001-T06 | todo | — | workplans/FT-WP-0001-telegram-identity-and-hall-channel.md | +| task | FT-WP-0001-T07 | todo | — | workplans/FT-WP-0001-telegram-identity-and-hall-channel.md | +| task | FT-WP-0001-T08 | todo | — | workplans/FT-WP-0001-telegram-identity-and-hall-channel.md | +| task | FT-WP-0001-T09 | todo | — | workplans/FT-WP-0001-telegram-identity-and-hall-channel.md | +| task | FT-WP-0001-T10 | todo | — | workplans/FT-WP-0001-telegram-identity-and-hall-channel.md | +| task | FT-WP-0001-T11 | todo | — | workplans/FT-WP-0001-telegram-identity-and-hall-channel.md | +| task | FT-WP-0001-T12 | todo | — | workplans/FT-WP-0001-telegram-identity-and-hall-channel.md | +| task | FT-WP-0001-T13 | todo | — | workplans/FT-WP-0001-telegram-identity-and-hall-channel.md | diff --git a/docs/provisioning.md b/docs/provisioning.md new file mode 100644 index 0000000..b8cf9c4 --- /dev/null +++ b/docs/provisioning.md @@ -0,0 +1,165 @@ +# 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/.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/.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. diff --git a/presence/telegram.example.yaml b/presence/telegram.example.yaml new file mode 100644 index 0000000..437c6c2 --- /dev/null +++ b/presence/telegram.example.yaml @@ -0,0 +1,44 @@ +# Worked example of a declared presence. +# +# The real instance belongs in pr-hall-of-helix/presence/telegram.yaml — this +# copy exists so the schema has something to be read against, and so the +# provisioner's tests have a fixture. Keep it in sync with the schema, not with +# the campaign. +presence: + schema_version: "0.1" + campaign: "hall-of-helix" + interface: "helix-forge-telegram-publishing" + + bot: + name: "HelixForge" + username_preference: + - "HelixForgeBot" + - "HelixForgePublishBot" + - "HelixForgeHallBot" + about: >- + Notes from the Hall of Helix — the people and sessions building HelixForge. + description: >- + HelixForge turns intent into structure, structure into capability, and + capability into lasting progress. This channel carries short accounts of + the work and the people doing it, each linking back to the entry it was + written from. + avatar: "presence/assets/helixforge-avatar.png" + + channels: + test: + title: "HelixForge — test" + description: "Private rendering checks. Nothing here is published." + visibility: private + admin_rights: [post_messages] + + public: + title: "Hall of Helix" + description: >- + Short notes on the work going into HelixForge, and the people doing it. + visibility: public + username_preference: + - "hallofhelix" + - "halloftheHelix" + admin_rights: [post_messages] + + linked_discussion_group: false diff --git a/presence/telegram.schema.yaml b/presence/telegram.schema.yaml new file mode 100644 index 0000000..555ea3d --- /dev/null +++ b/presence/telegram.schema.yaml @@ -0,0 +1,114 @@ +# Declared presence — schema for a campaign's Telegram surface. +# +# The instance lives in the campaign repo (pr-hall-of-helix/presence/telegram.yaml). +# 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" +title: "Telegram declared presence" +type: object +required: [presence] +additionalProperties: false + +properties: + presence: + type: object + required: [schema_version, campaign, interface, bot, channels] + additionalProperties: false + properties: + + schema_version: { const: "0.1" } + + campaign: + type: string + description: > + Campaign slug. Names the resolved-state file and the OpenBao subtree, + so it must be stable for the life of the presence. + pattern: "^[a-z0-9][a-z0-9-]*$" + + interface: + type: string + description: The FLUID interface that will publish through this presence. + const: "helix-forge-telegram-publishing" + + bot: + type: object + required: [name, about, description] + additionalProperties: false + properties: + name: + type: string + maxLength: 64 + description: Display name. What a reader encountering it cold sees first. + username_preference: + type: array + description: > + Candidate usernames in order. BotFather requires a globally unique + name ending in "bot" or "_bot", and the first choice is often taken. + Declaring fallbacks keeps apply from stopping on a collision; the + one actually issued is recorded in the resolved state. + items: { type: string, pattern: "^[A-Za-z][A-Za-z0-9_]{3,30}([Bb]ot|_bot)$" } + minItems: 1 + about: + type: string + maxLength: 120 + description: BotFather /setabouttext — shown on the profile before a chat starts. + description: + type: string + maxLength: 512 + description: BotFather /setdescription — shown in the empty chat. + avatar: + type: string + description: > + Repo-relative path to a square image in the campaign repo. + Provisioning is content-addressed on its digest, so replacing the + file is what triggers an update. + + channels: + type: object + required: [test, public] + additionalProperties: false + description: > + Exactly two. The test channel is created first and is where every + verification runs; nothing reaches the public channel until a person + has looked at a rendering in the test channel (Canon PUB-01). + properties: + test: { $ref: "#/$defs/channel" } + public: { $ref: "#/$defs/channel" } + + linked_discussion_group: + type: boolean + default: false + description: > + Whether the public channel has a linked group for comments. Off by + default: comments are an inbound surface, and this interface is + outbound only. Turning it on is a scope change, not a setting. + +$defs: + channel: + type: object + required: [title, visibility] + additionalProperties: false + properties: + title: + type: string + maxLength: 128 + description: + type: string + maxLength: 255 + visibility: + enum: [private, public] + username_preference: + type: array + description: Public channels only. Ordered candidates; a private channel must not declare one. + items: { type: string, pattern: "^[A-Za-z][A-Za-z0-9_]{4,31}$" } + admin_rights: + type: array + description: > + Rights granted to the bot. The provisioner clamps this: post_messages + is the only permitted value, and any other entry fails validation + rather than being silently dropped. Declared explicitly so that a + reader of the spec can see the boundary rather than infer it. + items: { const: post_messages } + default: [post_messages] + maxItems: 1 diff --git a/pressure/P-register-mismatch.yaml b/pressure/P-register.yaml similarity index 98% rename from pressure/P-register-mismatch.yaml rename to pressure/P-register.yaml index 782f0ed..8b09595 100644 --- a/pressure/P-register-mismatch.yaml +++ b/pressure/P-register.yaml @@ -43,3 +43,4 @@ fluid_pressure: linked_hypotheses: - "H-tg-visual" + - "H-tg-textonly" diff --git a/revisions/R-1.yaml b/revisions/R-1.yaml index 3486d9d..dfd6576 100644 --- a/revisions/R-1.yaml +++ b/revisions/R-1.yaml @@ -27,8 +27,8 @@ revision: # failing fast would turn a rate limit into a lost entry. timeout_ms: 30000 retry: - # Only GET is retried by the connector; POST /v1/hall-entries is - # idempotent on entry_id, but the retry decision stays with the adapter, + # Only GET is retried by the connector. POST /v1/channel-posts is + # idempotent on post_id, but the retry decision stays with the adapter, # which knows whether Telegram accepted the message. max_attempts: 1 circuit_breaker: diff --git a/workplans/FT-WP-0001-telegram-identity-and-hall-channel.md b/workplans/FT-WP-0001-telegram-identity-and-hall-channel.md index 7611376..4e3cf30 100644 --- a/workplans/FT-WP-0001-telegram-identity-and-hall-channel.md +++ b/workplans/FT-WP-0001-telegram-identity-and-hall-channel.md @@ -46,6 +46,14 @@ first experiment. See `docs/` and the handover package at What remains needs either a person with a Telegram account or a running adapter. +**T01–T03 have moved.** They were written as manual steps; they are now +provisioned from a declared specification by `FT-WP-0002`, and the three tasks +below are kept as the record of what must be true rather than as instructions to +follow. The Bot API cannot create a bot or a channel, so provisioning is an +MTProto client acting as a designated operator account (Canon INT-03); one +bounded human bootstrap remains, and `docs/provisioning.md` says exactly what it +is and why it cannot be removed. + ## T01 — Register the bot ```task @@ -55,14 +63,18 @@ priority: high state_hub_task_id: "a5449e86-3181-54b6-a0fe-e5c761b3e4ba" ``` -**Human step.** Register the HelixForge bot through BotFather (Canon BOT-01). -Bots are first-class Telegram accounts and need no phone number. +**Superseded by `FT-WP-0002` T04.** The bot is registered by the provisioner +from the declared presence spec, not by hand: BotFather `/newbot` driven over +MTProto, name, about text, description and avatar set from the spec, and the +token written straight to OpenBao without passing through a terminal. Consistent +with `HF-WP-0002` (bao.coulomb.social). -Set the bot's name, description and profile image to something a reader -encountering it cold would recognise as HelixForge. +What survives from the original step is the requirement, not the method: a +reader encountering the bot cold should recognise it as HelixForge. That is now +`bot.name` / `bot.about` / `bot.description` in the spec, where it is reviewable +before it is applied. -Write the token to OpenBao immediately; do not paste it anywhere else, not even -briefly. Consistent with `HF-WP-0002` (bao.coulomb.social). +Close this task when `FT-WP-0002` T04 has run. See `docs/provisioning.md`. ## T02 — Create the channels @@ -73,13 +85,21 @@ priority: high state_hub_task_id: "597ab1b7-5903-5a5d-a510-0a6946325404" ``` -**Human step.** Create the **private test channel first**, then the public -channel (Canon PUB-01). Add the bot as administrator with **Post Messages** -only — the intent forbids it from using any wider right, so granting one would -be a permission the system is not allowed to exercise. +**Superseded by `FT-WP-0002` T04.** Both channels are created by the +provisioner via `channels.createChannel`, private test channel first (Canon +PUB-01), with the bot added as administrator through `channels.editAdmin`. -Every verification below runs against the private channel. Nothing reaches the -public channel until the rendering has been seen by a person. +The ordering and the rights are no longer conventions a person is trusted to +follow. `apply` will not touch the public channel until the test channel has +recorded a successful publication, and it grants **Post Messages** and refuses +anything wider *even if the spec asks for it* — the intent forbids the system +from holding a right it may not exercise, so the grant is the right place to +enforce that. + +Every verification below still runs against the private channel, and nothing +reaches the public channel until a person has looked at a rendering. + +Close this task when `FT-WP-0002` T04 has run. ## T03 — Generate and store the redaction salt @@ -90,9 +110,15 @@ priority: high state_hub_task_id: "bbe5da73-e604-5dfe-b25f-fba802f67618" ``` -`openssl rand -hex 32`, stored in OpenBao beside the token, injected as -`FLUID_REDACTION_SALT`. It must never change: see `docs/observation.md` for why -rotating it silently invalidates every longitudinal comparison. +**Superseded by `FT-WP-0002` T05.** The provisioner generates 32 bytes if and +only if the OpenBao path is empty, and has no path that overwrites one. Injected +as `FLUID_REDACTION_SALT`. + +It must never change: see `docs/observation.md` for why rotating it silently +invalidates every longitudinal comparison. Making that a property of the tool +rather than a note in a workplan is the whole reason the step moved. + +Close this task when `FT-WP-0002` T05 has run. ## T04 — Record the intent and publish R-1 diff --git a/workplans/FT-WP-0002-declared-presence-provisioning.md b/workplans/FT-WP-0002-declared-presence-provisioning.md new file mode 100644 index 0000000..584806c --- /dev/null +++ b/workplans/FT-WP-0002-declared-presence-provisioning.md @@ -0,0 +1,178 @@ +--- +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 +--- + +# 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. + +## T01 — Bootstrap the operator account and session + +```task +id: FT-WP-0002-T01 +status: todo +priority: high +``` + +**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. + +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. + +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 +priority: high +``` + +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. + +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. + +## T03 — Implement `provision plan` + +```task +id: FT-WP-0002-T03 +status: todo +priority: high +``` + +Read the spec, validate it against the schema, read live state through MTProto, +read `presence/resolved/.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 +``` + +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/.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 +``` + +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 +``` + +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 +``` + +`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 +``` + +`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.