Implement provision plan: spec, resolved state, and the diff
Go, per the toolchain decision. cmd/provision with internal/spec,
internal/state and internal/plan; plan computation is pure and takes live
observation through an interface, so the refusal logic is testable without
a Telegram account. It runs against the real campaign spec today.
Separates two refusals the design had treated as one. A deferral is the
design working -- the public channel waiting on a checked rendering, normal
on every first run. A block is the world disagreeing with the state file:
drifted rights, a taken-over username, a bot that is no longer reachable.
Collapsed together, a first run could never apply anything, because it
always defers the public channel.
The rights clamp and the private/public username rule are enforced in Go
and asserted against the same cases the JSON schema rejects, since mirroring
the schema in code is a drift risk worth a test rather than a comment.
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
2026-09-04 21:25:49 +02:00
|
|
|
// Command provision reconciles a Telegram presence with its declared
|
|
|
|
|
// specification.
|
|
|
|
|
//
|
|
|
|
|
// It is the provisioning plane, and it is deliberately separate from the
|
|
|
|
|
// adapter: the adapter holds a bot token with post_messages and nothing else,
|
|
|
|
|
// and has no code path into here. A compromised adapter cannot create, rename,
|
|
|
|
|
// delete or re-permission anything.
|
|
|
|
|
//
|
|
|
|
|
// See docs/provisioning.md for the design and docs/seeding-runbook.md for the
|
|
|
|
|
// steps that precede the first run.
|
|
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
Implement the MTProto client, session bootstrap and apply
Completes the provisioner's write path. internal/tg drives BotFather as a
conversation rather than pretending it is an endpoint, creates channels,
claims usernames with fallbacks, and grants post_messages. internal/apply
sequences it: bot before administrator, test channel before public, and
state saved after every step that changed the world -- a channel that
exists but is unrecorded is worse than one that does not exist, because
the next run creates a second.
The operator session lives in OpenBao, not on disk. gotd's FileStorage
would leave a full-account credential in the working directory, where it
outlives the run and can be committed by accident.
The bot token goes straight from BotFather's reply to OpenBao and is
cleared from memory; if that write fails the error says how to recover by
hand and warns against re-running, since a retry creates a second bot.
Closes T05: the redaction salt is create-if-absent with no overwrite path,
and the test asserts it, because rotating it invalidates every longitudinal
comparison with no visible failure.
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
2026-09-04 21:39:30 +02:00
|
|
|
"context"
|
|
|
|
|
"errors"
|
Implement provision plan: spec, resolved state, and the diff
Go, per the toolchain decision. cmd/provision with internal/spec,
internal/state and internal/plan; plan computation is pure and takes live
observation through an interface, so the refusal logic is testable without
a Telegram account. It runs against the real campaign spec today.
Separates two refusals the design had treated as one. A deferral is the
design working -- the public channel waiting on a checked rendering, normal
on every first run. A block is the world disagreeing with the state file:
drifted rights, a taken-over username, a bot that is no longer reachable.
Collapsed together, a first run could never apply anything, because it
always defers the public channel.
The rights clamp and the private/public username rule are enforced in Go
and asserted against the same cases the JSON schema rejects, since mirroring
the schema in code is a drift risk worth a test rather than a comment.
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
2026-09-04 21:25:49 +02:00
|
|
|
"flag"
|
|
|
|
|
"fmt"
|
|
|
|
|
"os"
|
|
|
|
|
|
Implement the MTProto client, session bootstrap and apply
Completes the provisioner's write path. internal/tg drives BotFather as a
conversation rather than pretending it is an endpoint, creates channels,
claims usernames with fallbacks, and grants post_messages. internal/apply
sequences it: bot before administrator, test channel before public, and
state saved after every step that changed the world -- a channel that
exists but is unrecorded is worse than one that does not exist, because
the next run creates a second.
The operator session lives in OpenBao, not on disk. gotd's FileStorage
would leave a full-account credential in the working directory, where it
outlives the run and can be committed by accident.
The bot token goes straight from BotFather's reply to OpenBao and is
cleared from memory; if that write fails the error says how to recover by
hand and warns against re-running, since a retry creates a second bot.
Closes T05: the redaction salt is create-if-absent with no overwrite path,
and the test asserts it, because rotating it invalidates every longitudinal
comparison with no visible failure.
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
2026-09-04 21:39:30 +02:00
|
|
|
"github.com/tegwick/fluid-telegram/internal/apply"
|
Implement provision plan: spec, resolved state, and the diff
Go, per the toolchain decision. cmd/provision with internal/spec,
internal/state and internal/plan; plan computation is pure and takes live
observation through an interface, so the refusal logic is testable without
a Telegram account. It runs against the real campaign spec today.
Separates two refusals the design had treated as one. A deferral is the
design working -- the public channel waiting on a checked rendering, normal
on every first run. A block is the world disagreeing with the state file:
drifted rights, a taken-over username, a bot that is no longer reachable.
Collapsed together, a first run could never apply anything, because it
always defers the public channel.
The rights clamp and the private/public username rule are enforced in Go
and asserted against the same cases the JSON schema rejects, since mirroring
the schema in code is a drift risk worth a test rather than a comment.
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
2026-09-04 21:25:49 +02:00
|
|
|
"github.com/tegwick/fluid-telegram/internal/plan"
|
Implement the MTProto client, session bootstrap and apply
Completes the provisioner's write path. internal/tg drives BotFather as a
conversation rather than pretending it is an endpoint, creates channels,
claims usernames with fallbacks, and grants post_messages. internal/apply
sequences it: bot before administrator, test channel before public, and
state saved after every step that changed the world -- a channel that
exists but is unrecorded is worse than one that does not exist, because
the next run creates a second.
The operator session lives in OpenBao, not on disk. gotd's FileStorage
would leave a full-account credential in the working directory, where it
outlives the run and can be committed by accident.
The bot token goes straight from BotFather's reply to OpenBao and is
cleared from memory; if that write fails the error says how to recover by
hand and warns against re-running, since a retry creates a second bot.
Closes T05: the redaction salt is create-if-absent with no overwrite path,
and the test asserts it, because rotating it invalidates every longitudinal
comparison with no visible failure.
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
2026-09-04 21:39:30 +02:00
|
|
|
"github.com/tegwick/fluid-telegram/internal/secrets"
|
Implement provision plan: spec, resolved state, and the diff
Go, per the toolchain decision. cmd/provision with internal/spec,
internal/state and internal/plan; plan computation is pure and takes live
observation through an interface, so the refusal logic is testable without
a Telegram account. It runs against the real campaign spec today.
Separates two refusals the design had treated as one. A deferral is the
design working -- the public channel waiting on a checked rendering, normal
on every first run. A block is the world disagreeing with the state file:
drifted rights, a taken-over username, a bot that is no longer reachable.
Collapsed together, a first run could never apply anything, because it
always defers the public channel.
The rights clamp and the private/public username rule are enforced in Go
and asserted against the same cases the JSON schema rejects, since mirroring
the schema in code is a drift risk worth a test rather than a comment.
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
2026-09-04 21:25:49 +02:00
|
|
|
"github.com/tegwick/fluid-telegram/internal/spec"
|
|
|
|
|
"github.com/tegwick/fluid-telegram/internal/state"
|
Implement the MTProto client, session bootstrap and apply
Completes the provisioner's write path. internal/tg drives BotFather as a
conversation rather than pretending it is an endpoint, creates channels,
claims usernames with fallbacks, and grants post_messages. internal/apply
sequences it: bot before administrator, test channel before public, and
state saved after every step that changed the world -- a channel that
exists but is unrecorded is worse than one that does not exist, because
the next run creates a second.
The operator session lives in OpenBao, not on disk. gotd's FileStorage
would leave a full-account credential in the working directory, where it
outlives the run and can be committed by accident.
The bot token goes straight from BotFather's reply to OpenBao and is
cleared from memory; if that write fails the error says how to recover by
hand and warns against re-running, since a retry creates a second bot.
Closes T05: the redaction salt is create-if-absent with no overwrite path,
and the test asserts it, because rotating it invalidates every longitudinal
comparison with no visible failure.
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
2026-09-04 21:39:30 +02:00
|
|
|
tgc "github.com/tegwick/fluid-telegram/internal/tg"
|
Implement provision plan: spec, resolved state, and the diff
Go, per the toolchain decision. cmd/provision with internal/spec,
internal/state and internal/plan; plan computation is pure and takes live
observation through an interface, so the refusal logic is testable without
a Telegram account. It runs against the real campaign spec today.
Separates two refusals the design had treated as one. A deferral is the
design working -- the public channel waiting on a checked rendering, normal
on every first run. A block is the world disagreeing with the state file:
drifted rights, a taken-over username, a bot that is no longer reachable.
Collapsed together, a first run could never apply anything, because it
always defers the public channel.
The rights clamp and the private/public username rule are enforced in Go
and asserted against the same cases the JSON schema rejects, since mirroring
the schema in code is a drift risk worth a test rather than a comment.
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
2026-09-04 21:25:49 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const usage = `provision -- reconcile a Telegram presence with its declared spec
|
|
|
|
|
|
Implement the avatar and a preflight dry run
The avatar is now applied rather than deferred: BotFather's /setuserpic is a
conversation in which you send a photo, so the file is uploaded and sent as
a message. It is content addressed -- replacing the file is what triggers an
update, and the digest is recorded only after BotFather confirms, so a failed
upload retries rather than being remembered as done. The image is validated
before the conversation starts, because an image rejected halfway leaves the
bot registered without a picture.
Adds `provision preflight`: spec, avatar, OpenBao reachability, credentials,
session presence, salt and the resulting plan, checked in one run that writes
nothing and never contacts Telegram. Every failure it reports is one that
would otherwise surface after a phone number had been spent.
Two bugs it found immediately. The avatar path is documented as repo-relative
but resolved against the spec's own directory, so the real campaign spec
failed to find its own asset. And the OpenBao error named both variables when
only one was missing, sending the reader to check the one already set.
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
2026-09-04 22:12:56 +02:00
|
|
|
provision preflight --spec <path> check everything short of contacting telegram
|
Implement provision plan: spec, resolved state, and the diff
Go, per the toolchain decision. cmd/provision with internal/spec,
internal/state and internal/plan; plan computation is pure and takes live
observation through an interface, so the refusal logic is testable without
a Telegram account. It runs against the real campaign spec today.
Separates two refusals the design had treated as one. A deferral is the
design working -- the public channel waiting on a checked rendering, normal
on every first run. A block is the world disagreeing with the state file:
drifted rights, a taken-over username, a bot that is no longer reachable.
Collapsed together, a first run could never apply anything, because it
always defers the public channel.
The rights clamp and the private/public username rule are enforced in Go
and asserted against the same cases the JSON schema rejects, since mirroring
the schema in code is a drift risk worth a test rather than a comment.
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
2026-09-04 21:25:49 +02:00
|
|
|
provision plan --spec <path> [--check] show what would change; writes nothing
|
|
|
|
|
provision apply --spec <path> execute an approved plan
|
|
|
|
|
|
Implement the MTProto client, session bootstrap and apply
Completes the provisioner's write path. internal/tg drives BotFather as a
conversation rather than pretending it is an endpoint, creates channels,
claims usernames with fallbacks, and grants post_messages. internal/apply
sequences it: bot before administrator, test channel before public, and
state saved after every step that changed the world -- a channel that
exists but is unrecorded is worse than one that does not exist, because
the next run creates a second.
The operator session lives in OpenBao, not on disk. gotd's FileStorage
would leave a full-account credential in the working directory, where it
outlives the run and can be committed by accident.
The bot token goes straight from BotFather's reply to OpenBao and is
cleared from memory; if that write fails the error says how to recover by
hand and warns against re-running, since a retry creates a second bot.
Closes T05: the redaction salt is create-if-absent with no overwrite path,
and the test asserts it, because rotating it invalidates every longitudinal
comparison with no visible failure.
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
2026-09-04 21:39:30 +02:00
|
|
|
provision session bootstrap --campaign <slug> mint the operator session (interactive)
|
|
|
|
|
provision session check --campaign <slug> verify the stored session
|
Implement provision plan: spec, resolved state, and the diff
Go, per the toolchain decision. cmd/provision with internal/spec,
internal/state and internal/plan; plan computation is pure and takes live
observation through an interface, so the refusal logic is testable without
a Telegram account. It runs against the real campaign spec today.
Separates two refusals the design had treated as one. A deferral is the
design working -- the public channel waiting on a checked rendering, normal
on every first run. A block is the world disagreeing with the state file:
drifted rights, a taken-over username, a bot that is no longer reachable.
Collapsed together, a first run could never apply anything, because it
always defers the public channel.
The rights clamp and the private/public username rule are enforced in Go
and asserted against the same cases the JSON schema rejects, since mirroring
the schema in code is a drift risk worth a test rather than a comment.
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
2026-09-04 21:25:49 +02:00
|
|
|
|
|
|
|
|
Flags:
|
Implement the MTProto client, session bootstrap and apply
Completes the provisioner's write path. internal/tg drives BotFather as a
conversation rather than pretending it is an endpoint, creates channels,
claims usernames with fallbacks, and grants post_messages. internal/apply
sequences it: bot before administrator, test channel before public, and
state saved after every step that changed the world -- a channel that
exists but is unrecorded is worse than one that does not exist, because
the next run creates a second.
The operator session lives in OpenBao, not on disk. gotd's FileStorage
would leave a full-account credential in the working directory, where it
outlives the run and can be committed by accident.
The bot token goes straight from BotFather's reply to OpenBao and is
cleared from memory; if that write fails the error says how to recover by
hand and warns against re-running, since a retry creates a second bot.
Closes T05: the redaction salt is create-if-absent with no overwrite path,
and the test asserts it, because rotating it invalidates every longitudinal
comparison with no visible failure.
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
2026-09-04 21:39:30 +02:00
|
|
|
--spec path to the presence spec (in the campaign repo)
|
|
|
|
|
--root repo root holding presence/resolved/ (default: .)
|
|
|
|
|
--check exit non-zero if anything would change; for scheduled drift checks
|
|
|
|
|
--offline compute a plan without contacting Telegram
|
|
|
|
|
|
|
|
|
|
Credentials come from OpenBao via BAO_ADDR and BAO_TOKEN.
|
|
|
|
|
See docs/seeding-runbook.md.
|
Implement provision plan: spec, resolved state, and the diff
Go, per the toolchain decision. cmd/provision with internal/spec,
internal/state and internal/plan; plan computation is pure and takes live
observation through an interface, so the refusal logic is testable without
a Telegram account. It runs against the real campaign spec today.
Separates two refusals the design had treated as one. A deferral is the
design working -- the public channel waiting on a checked rendering, normal
on every first run. A block is the world disagreeing with the state file:
drifted rights, a taken-over username, a bot that is no longer reachable.
Collapsed together, a first run could never apply anything, because it
always defers the public channel.
The rights clamp and the private/public username rule are enforced in Go
and asserted against the same cases the JSON schema rejects, since mirroring
the schema in code is a drift risk worth a test rather than a comment.
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
2026-09-04 21:25:49 +02:00
|
|
|
`
|
|
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
|
if len(os.Args) < 2 {
|
|
|
|
|
fmt.Fprint(os.Stderr, usage)
|
|
|
|
|
os.Exit(2)
|
|
|
|
|
}
|
|
|
|
|
var err error
|
|
|
|
|
switch os.Args[1] {
|
|
|
|
|
case "plan":
|
Implement the MTProto client, session bootstrap and apply
Completes the provisioner's write path. internal/tg drives BotFather as a
conversation rather than pretending it is an endpoint, creates channels,
claims usernames with fallbacks, and grants post_messages. internal/apply
sequences it: bot before administrator, test channel before public, and
state saved after every step that changed the world -- a channel that
exists but is unrecorded is worse than one that does not exist, because
the next run creates a second.
The operator session lives in OpenBao, not on disk. gotd's FileStorage
would leave a full-account credential in the working directory, where it
outlives the run and can be committed by accident.
The bot token goes straight from BotFather's reply to OpenBao and is
cleared from memory; if that write fails the error says how to recover by
hand and warns against re-running, since a retry creates a second bot.
Closes T05: the redaction salt is create-if-absent with no overwrite path,
and the test asserts it, because rotating it invalidates every longitudinal
comparison with no visible failure.
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
2026-09-04 21:39:30 +02:00
|
|
|
err = cmdReconcile(os.Args[2:], false)
|
Implement provision plan: spec, resolved state, and the diff
Go, per the toolchain decision. cmd/provision with internal/spec,
internal/state and internal/plan; plan computation is pure and takes live
observation through an interface, so the refusal logic is testable without
a Telegram account. It runs against the real campaign spec today.
Separates two refusals the design had treated as one. A deferral is the
design working -- the public channel waiting on a checked rendering, normal
on every first run. A block is the world disagreeing with the state file:
drifted rights, a taken-over username, a bot that is no longer reachable.
Collapsed together, a first run could never apply anything, because it
always defers the public channel.
The rights clamp and the private/public username rule are enforced in Go
and asserted against the same cases the JSON schema rejects, since mirroring
the schema in code is a drift risk worth a test rather than a comment.
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
2026-09-04 21:25:49 +02:00
|
|
|
case "apply":
|
Implement the MTProto client, session bootstrap and apply
Completes the provisioner's write path. internal/tg drives BotFather as a
conversation rather than pretending it is an endpoint, creates channels,
claims usernames with fallbacks, and grants post_messages. internal/apply
sequences it: bot before administrator, test channel before public, and
state saved after every step that changed the world -- a channel that
exists but is unrecorded is worse than one that does not exist, because
the next run creates a second.
The operator session lives in OpenBao, not on disk. gotd's FileStorage
would leave a full-account credential in the working directory, where it
outlives the run and can be committed by accident.
The bot token goes straight from BotFather's reply to OpenBao and is
cleared from memory; if that write fails the error says how to recover by
hand and warns against re-running, since a retry creates a second bot.
Closes T05: the redaction salt is create-if-absent with no overwrite path,
and the test asserts it, because rotating it invalidates every longitudinal
comparison with no visible failure.
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
2026-09-04 21:39:30 +02:00
|
|
|
err = cmdReconcile(os.Args[2:], true)
|
Implement the avatar and a preflight dry run
The avatar is now applied rather than deferred: BotFather's /setuserpic is a
conversation in which you send a photo, so the file is uploaded and sent as
a message. It is content addressed -- replacing the file is what triggers an
update, and the digest is recorded only after BotFather confirms, so a failed
upload retries rather than being remembered as done. The image is validated
before the conversation starts, because an image rejected halfway leaves the
bot registered without a picture.
Adds `provision preflight`: spec, avatar, OpenBao reachability, credentials,
session presence, salt and the resulting plan, checked in one run that writes
nothing and never contacts Telegram. Every failure it reports is one that
would otherwise surface after a phone number had been spent.
Two bugs it found immediately. The avatar path is documented as repo-relative
but resolved against the spec's own directory, so the real campaign spec
failed to find its own asset. And the OpenBao error named both variables when
only one was missing, sending the reader to check the one already set.
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
2026-09-04 22:12:56 +02:00
|
|
|
case "preflight":
|
|
|
|
|
err = cmdPreflight(os.Args[2:])
|
Implement provision plan: spec, resolved state, and the diff
Go, per the toolchain decision. cmd/provision with internal/spec,
internal/state and internal/plan; plan computation is pure and takes live
observation through an interface, so the refusal logic is testable without
a Telegram account. It runs against the real campaign spec today.
Separates two refusals the design had treated as one. A deferral is the
design working -- the public channel waiting on a checked rendering, normal
on every first run. A block is the world disagreeing with the state file:
drifted rights, a taken-over username, a bot that is no longer reachable.
Collapsed together, a first run could never apply anything, because it
always defers the public channel.
The rights clamp and the private/public username rule are enforced in Go
and asserted against the same cases the JSON schema rejects, since mirroring
the schema in code is a drift risk worth a test rather than a comment.
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
2026-09-04 21:25:49 +02:00
|
|
|
case "session":
|
Implement the MTProto client, session bootstrap and apply
Completes the provisioner's write path. internal/tg drives BotFather as a
conversation rather than pretending it is an endpoint, creates channels,
claims usernames with fallbacks, and grants post_messages. internal/apply
sequences it: bot before administrator, test channel before public, and
state saved after every step that changed the world -- a channel that
exists but is unrecorded is worse than one that does not exist, because
the next run creates a second.
The operator session lives in OpenBao, not on disk. gotd's FileStorage
would leave a full-account credential in the working directory, where it
outlives the run and can be committed by accident.
The bot token goes straight from BotFather's reply to OpenBao and is
cleared from memory; if that write fails the error says how to recover by
hand and warns against re-running, since a retry creates a second bot.
Closes T05: the redaction salt is create-if-absent with no overwrite path,
and the test asserts it, because rotating it invalidates every longitudinal
comparison with no visible failure.
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
2026-09-04 21:39:30 +02:00
|
|
|
err = cmdSession(os.Args[2:])
|
Implement provision plan: spec, resolved state, and the diff
Go, per the toolchain decision. cmd/provision with internal/spec,
internal/state and internal/plan; plan computation is pure and takes live
observation through an interface, so the refusal logic is testable without
a Telegram account. It runs against the real campaign spec today.
Separates two refusals the design had treated as one. A deferral is the
design working -- the public channel waiting on a checked rendering, normal
on every first run. A block is the world disagreeing with the state file:
drifted rights, a taken-over username, a bot that is no longer reachable.
Collapsed together, a first run could never apply anything, because it
always defers the public channel.
The rights clamp and the private/public username rule are enforced in Go
and asserted against the same cases the JSON schema rejects, since mirroring
the schema in code is a drift risk worth a test rather than a comment.
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
2026-09-04 21:25:49 +02:00
|
|
|
case "-h", "--help", "help":
|
|
|
|
|
fmt.Print(usage)
|
|
|
|
|
return
|
|
|
|
|
default:
|
|
|
|
|
err = fmt.Errorf("unknown command %q", os.Args[1])
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
fmt.Fprintln(os.Stderr, "provision:", err)
|
|
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
Implement the MTProto client, session bootstrap and apply
Completes the provisioner's write path. internal/tg drives BotFather as a
conversation rather than pretending it is an endpoint, creates channels,
claims usernames with fallbacks, and grants post_messages. internal/apply
sequences it: bot before administrator, test channel before public, and
state saved after every step that changed the world -- a channel that
exists but is unrecorded is worse than one that does not exist, because
the next run creates a second.
The operator session lives in OpenBao, not on disk. gotd's FileStorage
would leave a full-account credential in the working directory, where it
outlives the run and can be committed by accident.
The bot token goes straight from BotFather's reply to OpenBao and is
cleared from memory; if that write fails the error says how to recover by
hand and warns against re-running, since a retry creates a second bot.
Closes T05: the redaction salt is create-if-absent with no overwrite path,
and the test asserts it, because rotating it invalidates every longitudinal
comparison with no visible failure.
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
2026-09-04 21:39:30 +02:00
|
|
|
func cmdReconcile(args []string, doApply bool) error {
|
|
|
|
|
name := "plan"
|
|
|
|
|
if doApply {
|
|
|
|
|
name = "apply"
|
|
|
|
|
}
|
|
|
|
|
fs := flag.NewFlagSet(name, flag.ExitOnError)
|
Implement provision plan: spec, resolved state, and the diff
Go, per the toolchain decision. cmd/provision with internal/spec,
internal/state and internal/plan; plan computation is pure and takes live
observation through an interface, so the refusal logic is testable without
a Telegram account. It runs against the real campaign spec today.
Separates two refusals the design had treated as one. A deferral is the
design working -- the public channel waiting on a checked rendering, normal
on every first run. A block is the world disagreeing with the state file:
drifted rights, a taken-over username, a bot that is no longer reachable.
Collapsed together, a first run could never apply anything, because it
always defers the public channel.
The rights clamp and the private/public username rule are enforced in Go
and asserted against the same cases the JSON schema rejects, since mirroring
the schema in code is a drift risk worth a test rather than a comment.
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
2026-09-04 21:25:49 +02:00
|
|
|
specPath := fs.String("spec", "", "path to the presence spec")
|
|
|
|
|
root := fs.String("root", ".", "repo root holding presence/resolved/")
|
|
|
|
|
check := fs.Bool("check", false, "exit non-zero if anything would change")
|
Implement the MTProto client, session bootstrap and apply
Completes the provisioner's write path. internal/tg drives BotFather as a
conversation rather than pretending it is an endpoint, creates channels,
claims usernames with fallbacks, and grants post_messages. internal/apply
sequences it: bot before administrator, test channel before public, and
state saved after every step that changed the world -- a channel that
exists but is unrecorded is worse than one that does not exist, because
the next run creates a second.
The operator session lives in OpenBao, not on disk. gotd's FileStorage
would leave a full-account credential in the working directory, where it
outlives the run and can be committed by accident.
The bot token goes straight from BotFather's reply to OpenBao and is
cleared from memory; if that write fails the error says how to recover by
hand and warns against re-running, since a retry creates a second bot.
Closes T05: the redaction salt is create-if-absent with no overwrite path,
and the test asserts it, because rotating it invalidates every longitudinal
comparison with no visible failure.
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
2026-09-04 21:39:30 +02:00
|
|
|
offline := fs.Bool("offline", false, "compute a plan without contacting Telegram")
|
Implement provision plan: spec, resolved state, and the diff
Go, per the toolchain decision. cmd/provision with internal/spec,
internal/state and internal/plan; plan computation is pure and takes live
observation through an interface, so the refusal logic is testable without
a Telegram account. It runs against the real campaign spec today.
Separates two refusals the design had treated as one. A deferral is the
design working -- the public channel waiting on a checked rendering, normal
on every first run. A block is the world disagreeing with the state file:
drifted rights, a taken-over username, a bot that is no longer reachable.
Collapsed together, a first run could never apply anything, because it
always defers the public channel.
The rights clamp and the private/public username rule are enforced in Go
and asserted against the same cases the JSON schema rejects, since mirroring
the schema in code is a drift risk worth a test rather than a comment.
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
2026-09-04 21:25:49 +02:00
|
|
|
fs.Parse(args)
|
|
|
|
|
|
|
|
|
|
if *specPath == "" {
|
Implement the MTProto client, session bootstrap and apply
Completes the provisioner's write path. internal/tg drives BotFather as a
conversation rather than pretending it is an endpoint, creates channels,
claims usernames with fallbacks, and grants post_messages. internal/apply
sequences it: bot before administrator, test channel before public, and
state saved after every step that changed the world -- a channel that
exists but is unrecorded is worse than one that does not exist, because
the next run creates a second.
The operator session lives in OpenBao, not on disk. gotd's FileStorage
would leave a full-account credential in the working directory, where it
outlives the run and can be committed by accident.
The bot token goes straight from BotFather's reply to OpenBao and is
cleared from memory; if that write fails the error says how to recover by
hand and warns against re-running, since a retry creates a second bot.
Closes T05: the redaction salt is create-if-absent with no overwrite path,
and the test asserts it, because rotating it invalidates every longitudinal
comparison with no visible failure.
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
2026-09-04 21:39:30 +02:00
|
|
|
return errors.New("--spec is required")
|
|
|
|
|
}
|
|
|
|
|
if doApply && *offline {
|
|
|
|
|
return errors.New("--offline cannot be combined with apply")
|
Implement provision plan: spec, resolved state, and the diff
Go, per the toolchain decision. cmd/provision with internal/spec,
internal/state and internal/plan; plan computation is pure and takes live
observation through an interface, so the refusal logic is testable without
a Telegram account. It runs against the real campaign spec today.
Separates two refusals the design had treated as one. A deferral is the
design working -- the public channel waiting on a checked rendering, normal
on every first run. A block is the world disagreeing with the state file:
drifted rights, a taken-over username, a bot that is no longer reachable.
Collapsed together, a first run could never apply anything, because it
always defers the public channel.
The rights clamp and the private/public username rule are enforced in Go
and asserted against the same cases the JSON schema rejects, since mirroring
the schema in code is a drift risk worth a test rather than a comment.
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
2026-09-04 21:25:49 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sp, digest, err := spec.Load(*specPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
statePath := state.Path(*root, sp.Campaign)
|
|
|
|
|
rs, err := state.Load(statePath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
Implement the MTProto client, session bootstrap and apply
Completes the provisioner's write path. internal/tg drives BotFather as a
conversation rather than pretending it is an endpoint, creates channels,
claims usernames with fallbacks, and grants post_messages. internal/apply
sequences it: bot before administrator, test channel before public, and
state saved after every step that changed the world -- a channel that
exists but is unrecorded is worse than one that does not exist, because
the next run creates a second.
The operator session lives in OpenBao, not on disk. gotd's FileStorage
would leave a full-account credential in the working directory, where it
outlives the run and can be committed by accident.
The bot token goes straight from BotFather's reply to OpenBao and is
cleared from memory; if that write fails the error says how to recover by
hand and warns against re-running, since a retry creates a second bot.
Closes T05: the redaction salt is create-if-absent with no overwrite path,
and the test asserts it, because rotating it invalidates every longitudinal
comparison with no visible failure.
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
2026-09-04 21:39:30 +02:00
|
|
|
ctx := context.Background()
|
Implement provision plan: spec, resolved state, and the diff
Go, per the toolchain decision. cmd/provision with internal/spec,
internal/state and internal/plan; plan computation is pure and takes live
observation through an interface, so the refusal logic is testable without
a Telegram account. It runs against the real campaign spec today.
Separates two refusals the design had treated as one. A deferral is the
design working -- the public channel waiting on a checked rendering, normal
on every first run. A block is the world disagreeing with the state file:
drifted rights, a taken-over username, a bot that is no longer reachable.
Collapsed together, a first run could never apply anything, because it
always defers the public channel.
The rights clamp and the private/public username rule are enforced in Go
and asserted against the same cases the JSON schema rejects, since mirroring
the schema in code is a drift risk worth a test rather than a comment.
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
2026-09-04 21:25:49 +02:00
|
|
|
|
Implement the MTProto client, session bootstrap and apply
Completes the provisioner's write path. internal/tg drives BotFather as a
conversation rather than pretending it is an endpoint, creates channels,
claims usernames with fallbacks, and grants post_messages. internal/apply
sequences it: bot before administrator, test channel before public, and
state saved after every step that changed the world -- a channel that
exists but is unrecorded is worse than one that does not exist, because
the next run creates a second.
The operator session lives in OpenBao, not on disk. gotd's FileStorage
would leave a full-account credential in the working directory, where it
outlives the run and can be committed by accident.
The bot token goes straight from BotFather's reply to OpenBao and is
cleared from memory; if that write fails the error says how to recover by
hand and warns against re-running, since a retry creates a second bot.
Closes T05: the redaction salt is create-if-absent with no overwrite path,
and the test asserts it, because rotating it invalidates every longitudinal
comparison with no visible failure.
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
2026-09-04 21:39:30 +02:00
|
|
|
if *offline {
|
Implement the avatar and a preflight dry run
The avatar is now applied rather than deferred: BotFather's /setuserpic is a
conversation in which you send a photo, so the file is uploaded and sent as
a message. It is content addressed -- replacing the file is what triggers an
update, and the digest is recorded only after BotFather confirms, so a failed
upload retries rather than being remembered as done. The image is validated
before the conversation starts, because an image rejected halfway leaves the
bot registered without a picture.
Adds `provision preflight`: spec, avatar, OpenBao reachability, credentials,
session presence, salt and the resulting plan, checked in one run that writes
nothing and never contacts Telegram. Every failure it reports is one that
would otherwise surface after a phone number had been spent.
Two bugs it found immediately. The avatar path is documented as repo-relative
but resolved against the spec's own directory, so the real campaign spec
failed to find its own asset. And the OpenBao error named both variables when
only one was missing, sending the reader to check the one already set.
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
2026-09-04 22:12:56 +02:00
|
|
|
p, err := plan.Compute(*specPath, sp, digest, rs, offlineLive{})
|
Implement the MTProto client, session bootstrap and apply
Completes the provisioner's write path. internal/tg drives BotFather as a
conversation rather than pretending it is an endpoint, creates channels,
claims usernames with fallbacks, and grants post_messages. internal/apply
sequences it: bot before administrator, test channel before public, and
state saved after every step that changed the world -- a channel that
exists but is unrecorded is worse than one that does not exist, because
the next run creates a second.
The operator session lives in OpenBao, not on disk. gotd's FileStorage
would leave a full-account credential in the working directory, where it
outlives the run and can be committed by accident.
The bot token goes straight from BotFather's reply to OpenBao and is
cleared from memory; if that write fails the error says how to recover by
hand and warns against re-running, since a retry creates a second bot.
Closes T05: the redaction salt is create-if-absent with no overwrite path,
and the test asserts it, because rotating it invalidates every longitudinal
comparison with no visible failure.
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
2026-09-04 21:39:30 +02:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
fmt.Print(p.Render())
|
|
|
|
|
fmt.Printf("\nnote: --offline, so nothing was observed live. this plan reflects\n"+
|
|
|
|
|
" %s and the spec only, and is not a drift check.\n", statePath)
|
|
|
|
|
if *check {
|
|
|
|
|
return errors.New("--check needs live observation; drop --offline")
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
Implement provision plan: spec, resolved state, and the diff
Go, per the toolchain decision. cmd/provision with internal/spec,
internal/state and internal/plan; plan computation is pure and takes live
observation through an interface, so the refusal logic is testable without
a Telegram account. It runs against the real campaign spec today.
Separates two refusals the design had treated as one. A deferral is the
design working -- the public channel waiting on a checked rendering, normal
on every first run. A block is the world disagreeing with the state file:
drifted rights, a taken-over username, a bot that is no longer reachable.
Collapsed together, a first run could never apply anything, because it
always defers the public channel.
The rights clamp and the private/public username rule are enforced in Go
and asserted against the same cases the JSON schema rejects, since mirroring
the schema in code is a drift risk worth a test rather than a comment.
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
2026-09-04 21:25:49 +02:00
|
|
|
|
Implement the MTProto client, session bootstrap and apply
Completes the provisioner's write path. internal/tg drives BotFather as a
conversation rather than pretending it is an endpoint, creates channels,
claims usernames with fallbacks, and grants post_messages. internal/apply
sequences it: bot before administrator, test channel before public, and
state saved after every step that changed the world -- a channel that
exists but is unrecorded is worse than one that does not exist, because
the next run creates a second.
The operator session lives in OpenBao, not on disk. gotd's FileStorage
would leave a full-account credential in the working directory, where it
outlives the run and can be committed by accident.
The bot token goes straight from BotFather's reply to OpenBao and is
cleared from memory; if that write fails the error says how to recover by
hand and warns against re-running, since a retry creates a second bot.
Closes T05: the redaction salt is create-if-absent with no overwrite path,
and the test asserts it, because rotating it invalidates every longitudinal
comparison with no visible failure.
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
2026-09-04 21:39:30 +02:00
|
|
|
store, err := secrets.NewFromEnv(sp.Campaign)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("%w\n(use --offline for a plan that does not contact Telegram)", err)
|
|
|
|
|
}
|
|
|
|
|
creds, err := tgc.LoadCredentials(ctx, store)
|
Implement provision plan: spec, resolved state, and the diff
Go, per the toolchain decision. cmd/provision with internal/spec,
internal/state and internal/plan; plan computation is pure and takes live
observation through an interface, so the refusal logic is testable without
a Telegram account. It runs against the real campaign spec today.
Separates two refusals the design had treated as one. A deferral is the
design working -- the public channel waiting on a checked rendering, normal
on every first run. A block is the world disagreeing with the state file:
drifted rights, a taken-over username, a bot that is no longer reachable.
Collapsed together, a first run could never apply anything, because it
always defers the public channel.
The rights clamp and the private/public username rule are enforced in Go
and asserted against the same cases the JSON schema rejects, since mirroring
the schema in code is a drift risk worth a test rather than a comment.
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
2026-09-04 21:25:49 +02:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
Implement the MTProto client, session bootstrap and apply
Completes the provisioner's write path. internal/tg drives BotFather as a
conversation rather than pretending it is an endpoint, creates channels,
claims usernames with fallbacks, and grants post_messages. internal/apply
sequences it: bot before administrator, test channel before public, and
state saved after every step that changed the world -- a channel that
exists but is unrecorded is worse than one that does not exist, because
the next run creates a second.
The operator session lives in OpenBao, not on disk. gotd's FileStorage
would leave a full-account credential in the working directory, where it
outlives the run and can be committed by accident.
The bot token goes straight from BotFather's reply to OpenBao and is
cleared from memory; if that write fails the error says how to recover by
hand and warns against re-running, since a retry creates a second bot.
Closes T05: the redaction salt is create-if-absent with no overwrite path,
and the test asserts it, because rotating it invalidates every longitudinal
comparison with no visible failure.
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
2026-09-04 21:39:30 +02:00
|
|
|
client := tgc.New(store, creds)
|
|
|
|
|
|
|
|
|
|
// No authenticator: reconciliation must never prompt. If the session is
|
|
|
|
|
// gone, that is a runbook step, not something to paper over mid-run.
|
|
|
|
|
return client.Run(ctx, nil, func(ctx context.Context, c *tgc.Client) error {
|
Implement the avatar and a preflight dry run
The avatar is now applied rather than deferred: BotFather's /setuserpic is a
conversation in which you send a photo, so the file is uploaded and sent as
a message. It is content addressed -- replacing the file is what triggers an
update, and the digest is recorded only after BotFather confirms, so a failed
upload retries rather than being remembered as done. The image is validated
before the conversation starts, because an image rejected halfway leaves the
bot registered without a picture.
Adds `provision preflight`: spec, avatar, OpenBao reachability, credentials,
session presence, salt and the resulting plan, checked in one run that writes
nothing and never contacts Telegram. Every failure it reports is one that
would otherwise surface after a phone number had been spent.
Two bugs it found immediately. The avatar path is documented as repo-relative
but resolved against the spec's own directory, so the real campaign spec
failed to find its own asset. And the OpenBao error named both variables when
only one was missing, sending the reader to check the one already set.
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
2026-09-04 22:12:56 +02:00
|
|
|
p, err := plan.Compute(*specPath, sp, digest, rs, tgc.Live{Ctx: ctx, Client: c, Resolved: rs})
|
Implement the MTProto client, session bootstrap and apply
Completes the provisioner's write path. internal/tg drives BotFather as a
conversation rather than pretending it is an endpoint, creates channels,
claims usernames with fallbacks, and grants post_messages. internal/apply
sequences it: bot before administrator, test channel before public, and
state saved after every step that changed the world -- a channel that
exists but is unrecorded is worse than one that does not exist, because
the next run creates a second.
The operator session lives in OpenBao, not on disk. gotd's FileStorage
would leave a full-account credential in the working directory, where it
outlives the run and can be committed by accident.
The bot token goes straight from BotFather's reply to OpenBao and is
cleared from memory; if that write fails the error says how to recover by
hand and warns against re-running, since a retry creates a second bot.
Closes T05: the redaction salt is create-if-absent with no overwrite path,
and the test asserts it, because rotating it invalidates every longitudinal
comparison with no visible failure.
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
2026-09-04 21:39:30 +02:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
fmt.Print(p.Render())
|
Implement provision plan: spec, resolved state, and the diff
Go, per the toolchain decision. cmd/provision with internal/spec,
internal/state and internal/plan; plan computation is pure and takes live
observation through an interface, so the refusal logic is testable without
a Telegram account. It runs against the real campaign spec today.
Separates two refusals the design had treated as one. A deferral is the
design working -- the public channel waiting on a checked rendering, normal
on every first run. A block is the world disagreeing with the state file:
drifted rights, a taken-over username, a bot that is no longer reachable.
Collapsed together, a first run could never apply anything, because it
always defers the public channel.
The rights clamp and the private/public username rule are enforced in Go
and asserted against the same cases the JSON schema rejects, since mirroring
the schema in code is a drift risk worth a test rather than a comment.
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
2026-09-04 21:25:49 +02:00
|
|
|
|
Implement the MTProto client, session bootstrap and apply
Completes the provisioner's write path. internal/tg drives BotFather as a
conversation rather than pretending it is an endpoint, creates channels,
claims usernames with fallbacks, and grants post_messages. internal/apply
sequences it: bot before administrator, test channel before public, and
state saved after every step that changed the world -- a channel that
exists but is unrecorded is worse than one that does not exist, because
the next run creates a second.
The operator session lives in OpenBao, not on disk. gotd's FileStorage
would leave a full-account credential in the working directory, where it
outlives the run and can be committed by accident.
The bot token goes straight from BotFather's reply to OpenBao and is
cleared from memory; if that write fails the error says how to recover by
hand and warns against re-running, since a retry creates a second bot.
Closes T05: the redaction salt is create-if-absent with no overwrite path,
and the test asserts it, because rotating it invalidates every longitudinal
comparison with no visible failure.
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
2026-09-04 21:39:30 +02:00
|
|
|
if *check && !p.Empty() {
|
|
|
|
|
return errors.New("presence has drifted from the spec")
|
Implement provision plan: spec, resolved state, and the diff
Go, per the toolchain decision. cmd/provision with internal/spec,
internal/state and internal/plan; plan computation is pure and takes live
observation through an interface, so the refusal logic is testable without
a Telegram account. It runs against the real campaign spec today.
Separates two refusals the design had treated as one. A deferral is the
design working -- the public channel waiting on a checked rendering, normal
on every first run. A block is the world disagreeing with the state file:
drifted rights, a taken-over username, a bot that is no longer reachable.
Collapsed together, a first run could never apply anything, because it
always defers the public channel.
The rights clamp and the private/public username rule are enforced in Go
and asserted against the same cases the JSON schema rejects, since mirroring
the schema in code is a drift risk worth a test rather than a comment.
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
2026-09-04 21:25:49 +02:00
|
|
|
}
|
Implement the MTProto client, session bootstrap and apply
Completes the provisioner's write path. internal/tg drives BotFather as a
conversation rather than pretending it is an endpoint, creates channels,
claims usernames with fallbacks, and grants post_messages. internal/apply
sequences it: bot before administrator, test channel before public, and
state saved after every step that changed the world -- a channel that
exists but is unrecorded is worse than one that does not exist, because
the next run creates a second.
The operator session lives in OpenBao, not on disk. gotd's FileStorage
would leave a full-account credential in the working directory, where it
outlives the run and can be committed by accident.
The bot token goes straight from BotFather's reply to OpenBao and is
cleared from memory; if that write fails the error says how to recover by
hand and warns against re-running, since a retry creates a second bot.
Closes T05: the redaction salt is create-if-absent with no overwrite path,
and the test asserts it, because rotating it invalidates every longitudinal
comparison with no visible failure.
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
2026-09-04 21:39:30 +02:00
|
|
|
if p.Blocked() {
|
|
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
|
|
|
|
if !doApply {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
if p.Empty() {
|
|
|
|
|
fmt.Println("\nnothing to apply")
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
fmt.Println("\napplying:")
|
|
|
|
|
return apply.Run(ctx, p, apply.Options{
|
|
|
|
|
Spec: sp, SpecDigest: digest, State: rs, StatePath: statePath,
|
|
|
|
|
Store: store, Client: c, Out: os.Stdout,
|
|
|
|
|
})
|
|
|
|
|
})
|
Implement provision plan: spec, resolved state, and the diff
Go, per the toolchain decision. cmd/provision with internal/spec,
internal/state and internal/plan; plan computation is pure and takes live
observation through an interface, so the refusal logic is testable without
a Telegram account. It runs against the real campaign spec today.
Separates two refusals the design had treated as one. A deferral is the
design working -- the public channel waiting on a checked rendering, normal
on every first run. A block is the world disagreeing with the state file:
drifted rights, a taken-over username, a bot that is no longer reachable.
Collapsed together, a first run could never apply anything, because it
always defers the public channel.
The rights clamp and the private/public username rule are enforced in Go
and asserted against the same cases the JSON schema rejects, since mirroring
the schema in code is a drift risk worth a test rather than a comment.
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
2026-09-04 21:25:49 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// offlineLive answers only what can be known without a session. It never claims
|
Implement the MTProto client, session bootstrap and apply
Completes the provisioner's write path. internal/tg drives BotFather as a
conversation rather than pretending it is an endpoint, creates channels,
claims usernames with fallbacks, and grants post_messages. internal/apply
sequences it: bot before administrator, test channel before public, and
state saved after every step that changed the world -- a channel that
exists but is unrecorded is worse than one that does not exist, because
the next run creates a second.
The operator session lives in OpenBao, not on disk. gotd's FileStorage
would leave a full-account credential in the working directory, where it
outlives the run and can be committed by accident.
The bot token goes straight from BotFather's reply to OpenBao and is
cleared from memory; if that write fails the error says how to recover by
hand and warns against re-running, since a retry creates a second bot.
Closes T05: the redaction salt is create-if-absent with no overwrite path,
and the test asserts it, because rotating it invalidates every longitudinal
comparison with no visible failure.
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
2026-09-04 21:39:30 +02:00
|
|
|
// something is fine; where it cannot tell, it reports the presence intact so a
|
|
|
|
|
// first run still renders, and the caller says plainly that nothing was observed.
|
Implement provision plan: spec, resolved state, and the diff
Go, per the toolchain decision. cmd/provision with internal/spec,
internal/state and internal/plan; plan computation is pure and takes live
observation through an interface, so the refusal logic is testable without
a Telegram account. It runs against the real campaign spec today.
Separates two refusals the design had treated as one. A deferral is the
design working -- the public channel waiting on a checked rendering, normal
on every first run. A block is the world disagreeing with the state file:
drifted rights, a taken-over username, a bot that is no longer reachable.
Collapsed together, a first run could never apply anything, because it
always defers the public channel.
The rights clamp and the private/public username rule are enforced in Go
and asserted against the same cases the JSON schema rejects, since mirroring
the schema in code is a drift risk worth a test rather than a comment.
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
2026-09-04 21:25:49 +02:00
|
|
|
type offlineLive struct{}
|
|
|
|
|
|
|
|
|
|
func (offlineLive) BotExists(string) (bool, error) { return true, nil }
|
|
|
|
|
func (offlineLive) ChannelAdminRights(int64) ([]string, error) {
|
|
|
|
|
return []string{spec.PostMessages}, nil
|
|
|
|
|
}
|
|
|
|
|
func (offlineLive) ChannelUsername(int64) (string, error) { return "", nil }
|