# Конфигурация Конвенция: *как* устроена и грузится конфигурация transcriber (TOML). Правила оформления кода (How), не спецификация поведения. **Взято из проекта jellybit.** Расхождения с сегодняшним кодом названы по месту. Главные: образец называется `config.dist.toml`, а не `config.example.toml`; комментариями снабжена половина полей; валидации на старте нет вовсе, кроме проверки пустых ключей внутри адаптеров. **Механизировано:** запрет `os.Getenv` — `forbidigo` в `.golangci.yml` ([go-linters.md](go-linters.md), «Механизировано»). Он держит правило «настройки приезжают из TOML»; `godotenv` в `main.go` по-прежнему загружает `.env`, но кладёт его в окружение процесса, а не в настройки приложения. ## Принципы - **Конфигурация — только TOML.** Переменные окружения для конфигурации **не используем**: окружение наследуется дочерними процессами и видно через `/proc//environ` — для секретов это слабее файла под `0600`. *Расхождение:* `main.go` зовёт `godotenv.Load()` и молча продолжает без файла. - Грузим **один раз при старте** в одну типизированную структуру `Config` (под-структуры по секциям). Дальше по коду читаем только её — чтения файла в прикладном коде нет, только загрузчик `internal/config`. - Конфиг **неизменяем** после старта; смена параметров — перезапуск процесса. ## Файл и поиск - Имя конфига по умолчанию — **`config.toml`**, ищется в **рабочем каталоге** процесса. - Путь переопределяется опцией **`-c path`** или **`--config=path`**. - Образец в репозитории — **`config.dist.toml`** (см. ниже); реальный `config.toml` не коммитится. ## config.dist.toml — самодокументируемый образец `config.dist.toml` коммитим как единый справочник по конфигу: все секции и все поля. **Каждое поле снабжаем комментарием**, из которого ясно: - **зачем** поле — что оно меняет в поведении; - **допустимые значения** — перечисление или границы; - **единицы измерения**, если применимы — секунды, байты, доля `0–1`. ```toml [server] port = # порт HTTP-сервера shutdown_timeout = # ждать мягкой остановки сервера, секунды force_shutdown_timeout = # ждать остановки воркеров, секунды users_while_list = ["<@name>"] # кому отвечает бот; строка автора Telegram ``` Значения намеренно заменены плейсхолдерами: предмет конвенции — форма комментария, а числа, совпадающие с фактом до цифры, от факта неотличимы и начинают врать молча при смене умолчания. Действующие умолчания и их смысл живут одним домом — таблица «Настройки с числовым значением» в [../database.md](../database.md); `config.dist.toml` — источник истины по составу полей. Секретные поля оставляем пустыми — значение приходит из выкладки (см. «Секреты»). *Расхождение:* секции `[server]` в `config.dist.toml` не хватает поля `users_while_list`, из-за чего бот на свежем конфиге отвечает отказом всем. *Расхождение:* адреса провайдера в секции `[auth]` образца заполнены примерами вида `https://auth.example.com/...`, а не оставлены пустыми: пустой адрес не говорит, какой формы значение здесь ждут. Пустым оставлен только `client_secret` — он и есть секрет. ## Поля по дискриминатору `type` Когда набор полей секции зависит от поля-дискриминатора `type` (выбор одного из бекендов или внешних сервисов), обязательность и опциональность полей определяет значение `type`, а не фиксированный список секции. - **Проверка — по `type`.** Для каждого поддерживаемого значения свой набор обязательных полей; поля других значений не требуются. Неизвестное значение — ошибка на старте с перечислением поддерживаемых. - **Образец — по `type`.** В `config.dist.toml`: - основное (умолчательное) значение **предзаполнено** рабочими значениями; - альтернативные — **блоками-комментариями ниже**, каждый со своим описанием полей (зачем, границы, единицы — как у обычных полей); - так из примера видны все варианты и поля каждого, не открывая код. Дискриминатора в transcriber пока нет; правило записано на случай второго распознавателя. ## Секреты Секреты доставляет **выкладка**, рендеря их прямо в `config.toml` (transcriber: Ansible из `pet-project-server`). Приложение просто читает TOML — отдельного слоя секретов в коде нет. Источник истины секрета — внешнее хранилище выкладки, не репозиторий и не окружение. - Секретные поля transcriber: `telegram.bot_token`, `yandex.speech_kit_api_key`, `yandex.object_storage_access_key_id`, `yandex.object_storage_secret_access_key`, `auth.client_secret`. - Отрендеренный `config.toml` (с секретами) **не коммитится**; права `0600`, владелец — пользователь процесса (`1000:1000`). - В `config.dist.toml` секретные поля — пустые строки. *Расхождение:* сейчас там стоят подсказки вида `your_..._here`, а не пустые строки, и загрузчик их не отличает от настоящего значения. - Загрузчик на старте проверяет, что обязательные секреты не пусты (ловит криво отрендеренный файл) — см. «Проверка и остановка на старте». - В логи секреты не попадают — см. [logging.md](logging.md), «Безопасность». - **Отказ загрузки настроек не несёт содержимого файла.** Текст такого отказа собирает библиотека разбора, и собирает она его из разбираемого куска: `toml.ParseError` кладёт в сообщение само значение («Invalid float value: %q»). Оборванная кавычка в строке секретного ключа — типовая поломка криво отрендеренного шаблона выкладки — уносит ключ в журнал контейнера целиком, а инвариант «секрет не покидает конфиг» помечен необратимым. Поэтому отказ разбора пересобирается своими словами: путь, строка, столбец и последний ключ, без сообщения библиотеки. Прочие отказы декодера (несовпадение типов, неподдерживаемый тип) собраны из имён ключей и типов, значений в них нет, и их текст остаётся как есть — иначе за разборчивость отказа платили бы там, где платить не за что. ## Проверка и остановка на старте Конфиг проверяем **на старте, до приёма трафика**. Негодный конфиг — лог `ERROR` и выход с ненулевым кодом, не стартуем наполовину. Что проверяем: - обязательные поля заданы; - каталоги хранилища существуют и доступны на запись; - границы числовых полей соблюдены; - ключи внешних сервисов не пусты. *Расхождение:* `LoadConfig` проверяет только существование файла и разбирает TOML. Пустые ключи Yandex ловятся в конструкторе распознавателя, и там процесс выходит с кодом 1. Единого места проверки нет. Под это расхождение больше не подпадают два ключа секции `[telegram]` — признак включения и ключ доступа, — и проверок у них две. Третий ключ секции, `update_timeout`, границ по-прежнему не проверяет никто, и ноль в нём обращает длинный опрос в непрерывный. Обязательность признака включения судит загрузчик — только разбор отличает «ключ не задан» от «ключ задан ложным», потому что нулевое значение `bool` у обоих одинаковое. Заполненность ключа доступа судит `TelegramConfig.Validate()` из `main.go`, рядом с проверкой `[auth]`: пустой `bot_token` при `enabled = true` — ошибка настройки и отказ старта. Непустой негодный по-прежнему судится при сборке клиента, до подъёма сервера. Нормирует это `openspec/specs/intake`, «Признак включения решает, поднимается ли вход Telegram». Секция `[auth]` — первая, у которой проверка своя и стоит на старте: `AuthConfig.Validate()` зовётся из `main.go` сразу после загрузки и роняет процесс с перечнем незаполненных ключей. Причина в цене умолчания: поднявшись с молча выключенным входом, сервис остался бы открытым наружу, а узнать об этом было бы неоткуда. Сообщение называет **имена ключей**, а не значения — значение `client_secret` в журнал попасть не должно. ## Структура в коде - Весь разбор и проверка — в `internal/config`; наружу отдаётся готовая `Config`. - Одна корневая структура `Config` с под-структурами по секциям. Перечень секций и полей здесь не повторяем: источник истины по составу — `config.dist.toml`, действующие числа — [../database.md](../database.md), «Настройки с числовым значением». Каталог данных задаётся одним ключом `[storage] data_dir` ([ADR](../adr/ADR-2026-08-12-single-data-dir-config-key.md)). - Умолчания задаются в `defaultConfig()`, файл их перекрывает. Новое поле требует правки обоих мест. - **Обязательное поле — поле, у которого умолчания нет намеренно.** Умолчание у такого поля было бы угаданным намерением, и одна из двух ошибок стала бы тихой. Форма записи: умолчания нет ни в `defaultConfig()` (причина — строкой комментария у самого поля), ни по нулевому значению типа; присутствие ключа судит **разбор** — `MetaData.IsDefined` из `toml.DecodeFile`, — потому что значение отличить «не задано» от «задано нулём» не позволяет. В `config.dist.toml` у поля стоит значение свежей установки. Первое такое поле — `telegram.enabled`.