diff --git a/AGENTS.md b/AGENTS.md index b52b809..342d057 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:** `FT-WP-` +**Workplan prefix:** `FLUID-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/FT-WP-NNNN-.md` +**File location:** `workplans/FLUID-WP-NNNN-.md` **Archived location:** finished workplans may move to -`workplans/archived/YYMMDD-FT-WP-NNNN-.md`. The `YYMMDD` prefix is +`workplans/archived/YYMMDD-FLUID-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 -`FT-WP-ADHOC-YYYY-MM-DD`, and task ids -`FT-WP-ADHOC-YYYY-MM-DD-T01`, etc. `FT-WP` includes its final `-WP` +`FLUID-WP-ADHOC-YYYY-MM-DD`, and task ids +`FLUID-WP-ADHOC-YYYY-MM-DD-T01`, etc. `FLUID-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: FT-WP-NNNN +id: FLUID-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: FT-WP-NNNN-T01 +id: FLUID-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: FT-WP-NNNN`) or a spawned workplan. Residual is a *role*, +`origin_ref: FLUID-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 3c46915..80e4fa5 100644 --- a/INTENT.md +++ b/INTENT.md @@ -7,53 +7,18 @@ updated: "2026-09-04" ## Why it exists -`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. +Fluid interface for telegram messaging and interaction platform. ## Governing principle -`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. +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. ## What it enables -- 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. +- 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. diff --git a/SCOPE.md b/SCOPE.md index 9814043..6209022 100644 --- a/SCOPE.md +++ b/SCOPE.md @@ -1,69 +1,32 @@ # SCOPE +> This file was generated by `statehub register`. Refine it as the repository +> boundaries become clearer. + ## One-liner -The deterministic delivery interface that publishes composed HelixForge posts to -Telegram, under a governed contract with a reconstructible audit trail. +Fluid interface for telegram messaging and interaction platform. ## Core Idea -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. +fluid-telegram exists to provide the capability described in INTENT.md. ## In Scope -- 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`). +- Maintain the repository's primary implementation. +- Keep docs, tests, and operational metadata current. ## Out of Scope -- **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. +- Own unrelated adjacent systems. +- Make irreversible operational decisions without human approval. ## Current State -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` +- Status: active; implementation and stability should be verified by the repo agent. ## Getting Oriented -- 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/` +- Start with: INTENT.md +- Agent instructions: AGENTS.md +- Workplans: workplans/ diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md deleted file mode 100644 index 69eb93d..0000000 --- a/WORK-RECORDS.md +++ /dev/null @@ -1,24 +0,0 @@ -# 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 deleted file mode 100644 index b8cf9c4..0000000 --- a/docs/provisioning.md +++ /dev/null @@ -1,165 +0,0 @@ -# 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 deleted file mode 100644 index 437c6c2..0000000 --- a/presence/telegram.example.yaml +++ /dev/null @@ -1,44 +0,0 @@ -# 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 deleted file mode 100644 index 555ea3d..0000000 --- a/presence/telegram.schema.yaml +++ /dev/null @@ -1,114 +0,0 @@ -# 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.yaml b/pressure/P-register-mismatch.yaml similarity index 98% rename from pressure/P-register.yaml rename to pressure/P-register-mismatch.yaml index 8b09595..782f0ed 100644 --- a/pressure/P-register.yaml +++ b/pressure/P-register-mismatch.yaml @@ -43,4 +43,3 @@ fluid_pressure: linked_hypotheses: - "H-tg-visual" - - "H-tg-textonly" diff --git a/revisions/R-1.yaml b/revisions/R-1.yaml index dfd6576..3486d9d 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/channel-posts is - # idempotent on post_id, but the retry decision stays with the adapter, + # Only GET is retried by the connector; POST /v1/hall-entries is + # idempotent on entry_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 4e3cf30..7611376 100644 --- a/workplans/FT-WP-0001-telegram-identity-and-hall-channel.md +++ b/workplans/FT-WP-0001-telegram-identity-and-hall-channel.md @@ -46,14 +46,6 @@ 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 @@ -63,18 +55,14 @@ priority: high state_hub_task_id: "a5449e86-3181-54b6-a0fe-e5c761b3e4ba" ``` -**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). +**Human step.** Register the HelixForge bot through BotFather (Canon BOT-01). +Bots are first-class Telegram accounts and need no phone number. -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. +Set the bot's name, description and profile image to something a reader +encountering it cold would recognise as HelixForge. -Close this task when `FT-WP-0002` T04 has run. See `docs/provisioning.md`. +Write the token to OpenBao immediately; do not paste it anywhere else, not even +briefly. Consistent with `HF-WP-0002` (bao.coulomb.social). ## T02 — Create the channels @@ -85,21 +73,13 @@ priority: high state_hub_task_id: "597ab1b7-5903-5a5d-a510-0a6946325404" ``` -**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`. +**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. -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. +Every verification below runs against the private channel. Nothing reaches the +public channel until the rendering has been seen by a person. ## T03 — Generate and store the redaction salt @@ -110,15 +90,9 @@ priority: high state_hub_task_id: "bbe5da73-e604-5dfe-b25f-fba802f67618" ``` -**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. +`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. ## 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 deleted file mode 100644 index dc02c9e..0000000 --- a/workplans/FT-WP-0002-declared-presence-provisioning.md +++ /dev/null @@ -1,187 +0,0 @@ ---- -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 -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. - -## T01 — Bootstrap the operator account and session - -```task -id: FT-WP-0002-T01 -status: todo -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. - -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 -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. - -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 -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/.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 -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/.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 -state_hub_task_id: "d5fd905a-6abd-5ae1-9105-abc4c2a7127e" -``` - -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 -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. - -## T07 — Drift check - -```task -id: FT-WP-0002-T07 -status: todo -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-1 §7 exists to prevent. - -## T08 — Settle the §7 reading - -```task -id: FT-WP-0002-T08 -status: todo -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 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.