Loosen the all-or-nothing stance on provisioning: where a platform has no API to seed access, a guided runbook is the right answer. docs/seeding-runbook.md covers the three Telegram steps that cannot be automated, and each carries a "why not automated" line so the judgement can be revisited rather than inherited. Telegram's steps resist automation incidentally -- nobody built the endpoints -- unlike a control such as KYC, which resists by design and where a weak component would be a defect rather than an opening. Fix the schema's $id, which was a relative path and broke $ref resolution in ordinary validators, and add the visibility/username constraint the field descriptions already claimed. Both specs now validate, and the rejections are tested. 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
130 lines
4.8 KiB
YAML
130 lines
4.8 KiB
YAML
# 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"
|
|
# No $id: this schema is identified by its path in the repo. A relative $id is
|
|
# not a resolvable URI and breaks "$ref" resolution in ordinary validators.
|
|
title: "Telegram declared presence"
|
|
type: object
|
|
required: [presence]
|
|
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
|
|
# A private channel has no username to claim. Declaring one is a mistake
|
|
# about what is being created, so it fails here rather than being ignored
|
|
# by apply -- a silently dropped field is how a spec stops describing
|
|
# what actually exists.
|
|
allOf:
|
|
- if:
|
|
properties: { visibility: { const: private } }
|
|
required: [visibility]
|
|
then:
|
|
not: { required: [username_preference] }
|
|
- if:
|
|
properties: { visibility: { const: public } }
|
|
required: [visibility]
|
|
then:
|
|
required: [username_preference]
|
|
properties:
|
|
title:
|
|
type: string
|
|
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
|