Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
2793888
feat(codex): complete the Codex sign-in from chat with /login
KorotkovAlex Jul 29, 2026
97b4ea6
style(codex): clear the lint gate for the /login sign-in flow
KorotkovAlex Jul 29, 2026
0ee85d2
fix(codex): close the auth gate for every run source and keep the cod…
KorotkovAlex Jul 29, 2026
f3175fd
fix(codex): decide code visibility by destination, and refuse before …
KorotkovAlex Jul 29, 2026
d41289a
fix(codex): confirm the sign-in by its credential, and keep the code …
KorotkovAlex Jul 29, 2026
470b0c6
fix(codex): gate InjectAuto, the sixth run source my comment claimed …
KorotkovAlex Jul 29, 2026
7721f10
fix(codex): bound the unauthorized notice, and finish the de-duplication
KorotkovAlex Jul 29, 2026
9961147
fix(codex): gate /login cancel too, and make the docs match the flag
KorotkovAlex Jul 29, 2026
13cafa6
fix(codex): hold a login attempt behind one pointer so its state cann…
KorotkovAlex Jul 29, 2026
caac95f
fix(codex): say where /login works, and never leave a sign-in silent
KorotkovAlex Jul 29, 2026
f8bdcd1
fix(codex): keep /login off the VK poll loop, and answer only real me…
KorotkovAlex Jul 29, 2026
c454c94
fix(codex): one work decision, one bounded sender, and a test that me…
KorotkovAlex Jul 29, 2026
1011006
fix(codex): carry the attachment with the work decision, not just its…
KorotkovAlex Jul 29, 2026
9095cff
fix(codex): name the work kind in logs, share the greeting and the te…
KorotkovAlex Jul 29, 2026
70b3934
fix(codex): pause the follow-up sweep instead of destroying what it t…
KorotkovAlex Jul 29, 2026
5be334e
fix(codex): let the linter keep catching forgotten cases, and log ref…
KorotkovAlex Jul 29, 2026
bb406e9
fix(codex): drop the linter scaffolding, name the blocked user, sharp…
KorotkovAlex Jul 29, 2026
8a26494
fix(codex): stop charging the user's rate limit for a run that cannot…
KorotkovAlex Jul 29, 2026
b303855
fix(codex): pause the cron tick too, so a blocked fire does not lose …
KorotkovAlex Jul 29, 2026
dffcb50
fix(codex): one "already told them" registry, owned by the state it d…
KorotkovAlex Jul 29, 2026
1520853
fix(codex): keep the user's answer separate from the background one
KorotkovAlex Jul 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions adapters/telegram/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@ AI_BACKEND=claude # claude | codex | openai-compatible
# CODEX_HOME/auth.json (persisted in the codex_home volume) or CODEX_ACCESS_TOKEN.
# Keep CODEX_API_KEY empty in subscription mode: startup fails if it is set, so a
# subscription deploy cannot accidentally switch to usage-based API billing.
#
# First sign-in needs no shell on the host: the bot starts unauthorized and an
# allow-listed user sends /login IN A DIRECT MESSAGE (the reply carries a one-time
# code, so starting a sign-in is refused in group chats). Runs are blocked until
# it completes. CODEX_REQUIRE_AUTH=false only removes the block — without
# auth.json or CODEX_ACCESS_TOKEN runs then fail inside the Codex CLI instead.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MINOR: Новый блок про /login не упоминает единственный случай, когда старт всё ещё падает

  • Проблема: Блок обещает: «бот стартует неавторизованным, allow-listed пользователь делает /login, шелл на хосте не нужен». Но в internal/airunner/backend.go (BuildWithPendingLogin) есть явная ветка: при пустом CODEX_HOME послабление НЕ применяется и старт остаётся фатальным — иначе бот поднялся бы навечно заблокированным, а «успешный» /login записал бы credential в пустой путь. Оператор, который явно выставит CODEX_HOME= (а в env.j2 это CODEX_HOME={{ codex_home }}, то есть пустая ansible-переменная отрендерится в пустое значение), получит crash-loop вместо обещанного «стартует и ждёт /login». Ровно этот блок текста и читают, когда так делают.
  • Решение: Дописать в конец блока одну строку: # CODEX_HOME must stay non-empty: with an empty value startup still fails, since there would be nowhere to persist the login. Продублировать её же в adapters/telegram/deploy/roles/claude_tg_bot/templates/env.j2 рядом со строками про CODEX_REQUIRE_AUTH.

# CODEX_HOME must stay non-empty: with an empty value startup still fails, since

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MINOR: Утверждение про CODEX_HOME неверно при CODEX_REQUIRE_AUTH=false

  • Проблема: Фраза «CODEX_HOME must stay non-empty: with an empty value startup still fails» идёт сразу после предложения про CODEX_REQUIRE_AUTH=false и читается как безусловная, но это не так: ValidateCodex (internal/config/config.go:441-442) при CodexRequireAuth=false возвращает nil до всякой проверки, поэтому guard на пустой CODEX_HOME в airunner.BuildWithPendingLogin:112 просто не достигается, и бот стартует. В этой конфигурации childEnv (core/codexauth/codexauth.go:93-96) не выставляет CODEX_HOME, логин пишет auth.json в дефолт CLI мимо персистентного тома, а authFile() пуст — и после фактически успешного входа outcome (codexauth.go:686-690) отвечает «finished the sign-in but saved no credential in » с пустым путём. То же самое продублировано в env.j2 (A67).
  • Решение: Сделать утверждение истинным в коде: в internal/config.ValidateCodex для ветки CodexAuthSubscription возвращать ошибку при strings.TrimSpace(c.CodexHome) == "" ДО проверки CodexRequireAuth. Тогда пустой CODEX_HOME падает на старте в любой конфигурации, guard в

...(truncated)

# there would be nowhere to persist the login.
# Full flow, and who ends up owning the Codex account:
# docs/codex-integration-plan.md#subscription-setup
CODEX_AUTH_MODE=subscription # subscription | billing
CODEX_BIN=codex
CODEX_HOME=/home/claude/.codex
Expand Down
30 changes: 12 additions & 18 deletions adapters/telegram/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"strings"

"github.com/go-telegram/bot/models"

"github.com/duckbugio/flock/core/chat"
)

// CommandName returns the slash command addressed at the start of msg,
Expand Down Expand Up @@ -82,22 +84,14 @@ func StripCommandMention(text, botUsername string) string {
return token[:at] + rest
}

// HelpText is the static usage message replied to an allowed user who sends
// /help. It lists the slash commands the adapter understands. It is an
// engineering artifact (professional English, no duck flavor) and never reaches
// the Claude Runner.
const HelpText = "Flock Telegram assistant — available commands:\n\n" +
"/help — show this message\n" +
"/new — start a fresh session (forget the current conversation)\n" +
"/stop — stop the run currently in progress\n" +
"/schedule — manage scheduled jobs (when enabled)\n" +
"/goal <criterion> — arm a goal an independent evaluator re-checks after every run " +
"(/goal off to disarm)\n\n" +
"Send any other message to run it through the assistant."
// helpTitle names the transport; everything below it is shared with the VK
// adapter (chat.HelpBody), so the two cannot drift apart.
const helpTitle = "Flock Telegram assistant — available commands:\n\n"

// HelpText renders the usage message replied to an allowed user who sends /help.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: Условие показа /login не учитывает CODEX_ACCESS_TOKEN

  • Проблема: Видимость /login (и в меню, и в /help) завязана на Manager.Applicable() = codex + subscription. Но деплой с заданным CODEX_ACCESS_TOKEN — это ровно тот случай, который PR исключает для Claude и codex billing: интерактивного входа там нет, и /login ответит «Codex is authorized with a configured access token. No browser login needed». То есть меню и help ведут к команде, которая говорит «не нужно» — формулировка, которой в reservedBotCommands обоснован пропуск /login.
  • Решение: Развести два предиката в codexauth: Applicable() оставить как есть, чтобы /login force остался доступен (замена корпоративного токена на личную подписку — валидный сценарий), а для reservedBotCommands и chat.LoginVisibilityFor использовать отдельный LoginAdvertised() = Applicable() && !m.cfg.HasAccessToken. В TestMenuOmitsLoginWithoutAnInteractiveSignIn добавить кейс с HasAccessToken: true.

// It is a thin alias over the shared render, so this adapter owns only its title.
func HelpText(login chat.LoginVisibility) string { return chat.HelpText(helpTitle, login) }

// WelcomeText is the static usage message replied to an allowed user who sends
// /start. It is a short greeting prepended to the command help so a brand-new
// user immediately sees what the bot does and how to use it. Like HelpText it is
// an engineering artifact (plain English, no duck flavor) and never reaches the
// Claude Runner — the duck greeting comes from the model on a real message.
const WelcomeText = "Hi! I'm the Flock assistant.\n\n" + HelpText
// WelcomeText is the reply to /start: the shared greeting prepended to the help,
// so a brand-new user immediately sees what the bot does and how to use it.
func WelcomeText(login chat.LoginVisibility) string { return chat.WelcomeText(helpTitle, login) }
21 changes: 15 additions & 6 deletions adapters/telegram/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"testing"

"github.com/go-telegram/bot/models"

"github.com/duckbugio/flock/core/chat"
)

// botCmd builds a message whose leading token is a bot_command entity, the shape
Expand Down Expand Up @@ -96,12 +98,19 @@ func TestStripCommandMention(t *testing.T) {
// it can never submit a run); the constant IS the entire payload the handler
// sends.
func TestHelpTextListsCommands(t *testing.T) {
if HelpText == "" {
t.Fatal("HelpText is empty")
}
for _, cmd := range []string{"/help", "/new", "/stop"} {
if !strings.Contains(HelpText, cmd) {
t.Fatalf("HelpText does not mention %q:\n%s", cmd, HelpText)
// Both renders: production picks between them with LoginVisibilityFor, and the
// WITHOUT-login one is what every Claude, Codex-billing and access-token
// deployment gets — so testing only the other would leave the common case
// uncovered.
for _, visibility := range []chat.LoginVisibility{chat.WithLogin, chat.WithoutLogin} {
got := HelpText(visibility)
if got == "" {
t.Fatalf("HelpText(%v) is empty", visibility)
}
for _, cmd := range []string{"/help", "/new", "/stop"} {
if !strings.Contains(got, cmd) {
t.Fatalf("HelpText(%v) does not mention %q:\n%s", visibility, cmd, got)
}
}
}
}
9 changes: 9 additions & 0 deletions adapters/telegram/deploy/roles/claude_tg_bot/templates/env.j2
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ CLAUDE_MAX_COST_PER_REQUEST={{ claude_max_cost_per_request }}

# === CODEX AUTH ===
# subscription: use persisted {{ codex_home }}/auth.json or CODEX_ACCESS_TOKEN.
# With neither, the bot starts unauthorized and an allow-listed user completes

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MINOR: env.j2 не упоминает требование личного сообщения, в отличие от .env.example

  • Проблема: Шаблон описывает вход через /login, но умалчивает главное ограничение безопасности: команда работает только в личке и отклоняется в беседе (LoginPrivateOnlyText). Сосед по смыслу — adapters/telegram/.env.example:25 — это явно пишет. Оператор читает именно отрендеренный .env на сервере, пойдёт вводить /login в рабочей беседе и получит отказ без понимания причины.
  • Решение: Дописать ограничение и его причину в ту же строку комментария: # the one-time sign-in from the chat with /login IN A DIRECT MESSAGE (the reply / # carries a one-time code that authorizes an account for the whole bot, so it is / # refused in group chats; the bot posts a link + a one-time code).

# the one-time sign-in from the chat with /login IN A DIRECT MESSAGE. Runs are
# blocked until it lands. CODEX_REQUIRE_AUTH=false only removes the block —
# without auth.json or CODEX_ACCESS_TOKEN runs then fail inside the Codex CLI
# instead.
# CODEX_HOME must stay non-empty: with an empty value startup still fails,
# since there would be nowhere to persist the login.
# Full flow, and who ends up owning the Codex account:
# docs/codex-integration-plan.md#subscription-setup
# billing: requires CODEX_API_KEY and CODEX_BILLING_ACK=true.
CODEX_BIN={{ codex_bin }}
CODEX_MODEL={{ codex_model }}
Expand Down
26 changes: 14 additions & 12 deletions adapters/vk/commands.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
package vk

// HelpText is the static usage message for the VK adapter. It mirrors the
// Telegram adapter's HelpText and lists VK's command surface: VK has no native
// slash-command UI, so these are plain text commands the receiver intercepts. It
// is an engineering artifact (professional English, no duck flavor).
const HelpText = "Flock VK assistant — available commands:\n\n" +
"/help — show this message\n" +
"/new — start a fresh session (forget the current conversation)\n" +
"/stop — stop the run currently in progress\n" +
"/schedule — manage scheduled jobs (when enabled)\n" +
"/goal <criterion> — arm a goal an independent evaluator re-checks after every run " +
"(/goal off to disarm)\n\n" +
"Send any other message to run it through the assistant."
import "github.com/duckbugio/flock/core/chat"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MINOR: «Не могут разойтись» — но это две независимые копии строки

  • Проблема: Комментарий утверждает: «It is listed CONDITIONALLY, on the same applicability test the Telegram command menu uses, so the two adapters and the menu cannot disagree». На деле loginHelpLine — это две независимые копии литерала (adapters/vk/commands.go:21 и adapters/telegram/commands.go:103), плюс третья, иначе сформулированная копия описания в core/chat/reserved.go («Sign in to Codex on a subscription (other backends need no login)»). Общий у адаптеров только предикат Applicable(), а сам текст ничем не связан: правка в одном адаптере молча разойдётся с другим, и ни один тест этого не поймает — TestHelpListsLoginOnlyWhereItApplies в обоих пакетах проверяет лишь наличие подстроки /login.
  • Решение: Вынести строку в единственное место рядом с каноническим набором — в core/chat/reserved.go — и брать её обоими адаптерами: go // core/chat/reserved.go const LoginHelpLine = "/login — sign in to Codex on a subscription (/login status, /login cancel)\n" и в обоих commands.go: `return helpHeader + chat.LoginHel

...(truncated)

// helpTitle names the transport; everything below it is shared with the Telegram
// adapter (chat.HelpBody), so the two cannot drift apart. VK has no native
// slash-command UI, so these are plain text commands the receiver intercepts.
const helpTitle = "Flock VK assistant — available commands:\n\n"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: Doc-комментарий ссылается на несуществующий параметр withLogin

  • Проблема: adapters/vk/commands.go: комментарий говорит «withLogin adds the /login line», а параметр функции называется login (chat.LoginVisibility). В Telegram-адаптере аналогичный комментарий уже согласован с именем параметра, так что расходится только VK.
  • Решение: Заменить «withLogin» на «login» в комментарии к HelpText в adapters/vk/commands.go.

// helpText renders the usage message. It is a thin alias over the shared render,
// so this adapter owns only its title. Unexported like welcomeText beside it:
// nothing outside this package renders VK's help.
func helpText(login chat.LoginVisibility) string { return chat.HelpText(helpTitle, login) }

// welcomeText is the reply to /start, mirroring the Telegram adapter.
func welcomeText(login chat.LoginVisibility) string { return chat.WelcomeText(helpTitle, login) }

// goalUsageText is the /goal usage reply, mirroring the Telegram adapter.
const goalUsageText = "Usage:\n" +
Expand Down
Loading