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 <noreply@anthropic.com> 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
This commit is contained in:
parent
3dae4ae74c
commit
5a785b9d9f
11 changed files with 670 additions and 46 deletions
16
AGENTS.md
16
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/<task_id>" \
|
|||
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-<slug>.md`
|
||||
**File location:** `workplans/FT-WP-NNNN-<slug>.md`
|
||||
|
||||
**Archived location:** finished workplans may move to
|
||||
`workplans/archived/YYMMDD-FLUID-WP-NNNN-<slug>.md`. The `YYMMDD` prefix is
|
||||
`workplans/archived/YYMMDD-FT-WP-NNNN-<slug>.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: "<uuid>" # 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:
|
||||
|
|
|
|||
51
INTENT.md
51
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.
|
||||
|
|
|
|||
63
SCOPE.md
63
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/`
|
||||
|
|
|
|||
24
WORK-RECORDS.md
Normal file
24
WORK-RECORDS.md
Normal file
|
|
@ -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 |
|
||||
165
docs/provisioning.md
Normal file
165
docs/provisioning.md
Normal file
|
|
@ -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/<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.
|
||||
44
presence/telegram.example.yaml
Normal file
44
presence/telegram.example.yaml
Normal file
|
|
@ -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
|
||||
114
presence/telegram.schema.yaml
Normal file
114
presence/telegram.schema.yaml
Normal file
|
|
@ -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
|
||||
|
|
@ -43,3 +43,4 @@ fluid_pressure:
|
|||
|
||||
linked_hypotheses:
|
||||
- "H-tg-visual"
|
||||
- "H-tg-textonly"
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
178
workplans/FT-WP-0002-declared-presence-provisioning.md
Normal file
178
workplans/FT-WP-0002-declared-presence-provisioning.md
Normal file
|
|
@ -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/<campaign>.yaml`, print a diff. Write nothing.
|
||||
|
||||
Validation is where the rights clamp lives: an `admin_rights` entry other than
|
||||
`post_messages` fails here, loudly, rather than being quietly dropped. The intent
|
||||
forbids the system from holding a right it may not exercise, and a spec is the
|
||||
cheapest place to catch someone asking for one.
|
||||
|
||||
`plan` must be honest about what it cannot see. A username's availability is not
|
||||
knowable without attempting it, so the plan says "will attempt, with fallbacks"
|
||||
rather than promising an outcome.
|
||||
|
||||
## 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/<campaign>.yaml` with the spec digest.
|
||||
|
||||
Three refusals carry the weight, and each is a real failure mode rather than a
|
||||
hypothetical: there is **no destroy path** — removing a channel from the spec
|
||||
warns, because deleting one destroys its subscribers and history irreversibly;
|
||||
**drift on a destructive field stops the run** — a demoted bot or a taken-over
|
||||
username exits non-zero rather than being "fixed" by a tool that does not
|
||||
understand what happened; and **the public channel is not touched** until the
|
||||
test channel has recorded a successful publication.
|
||||
|
||||
## 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue