# Конфигурация Конвенция: *как* устроена и грузится конфигурация transcriber (TOML). Правила оформления кода (How), не спецификация поведения. **Взято из проекта jellybit.** Расхождения с сегодняшним кодом названы по месту. Главные: комментариями снабжена половина полей; единого места проверки на старте нет: у секций `[auth]` и `[pipeline]` свой `Validate()` в точке входа, а пустые ключи `[yandex]` ловит конструктор распознавателя. **Механизировано:** запрет `os.Getenv` — `forbidigo` в `.golangci.yml` ([go-linters.md](go-linters.md), «Механизировано»). Он держит правило «настройки приезжают из TOML»; `godotenv` в `cmd/transcriber` по-прежнему загружает `.env`, но кладёт его в окружение процесса, а не в настройки приложения. ## Принципы - **Конфигурация — только TOML.** Переменные окружения для конфигурации **не используем**: окружение наследуется дочерними процессами и видно через `/proc//environ` — для секретов это слабее файла под `0600`. *Расхождение:* `cmd/transcriber` зовёт `godotenv.Load()` и молча продолжает без файла. - Грузим **один раз при старте** в одну типизированную структуру `Config` (под-структуры по секциям). Дальше по коду читаем только её — чтения файла в прикладном коде нет, только загрузчик `internal/config`. - Конфиг **неизменяем** после старта; смена параметров — перезапуск процесса. ## Файл и поиск - Имя конфига по умолчанию — **`config.toml`**, ищется в **рабочем каталоге** процесса. - Путь переопределяется опцией **`-c path`** или **`--config=path`**. - Образец в репозитории — **`config.example.toml`** (см. ниже); реальный `config.toml` не коммитится. ## config.example.toml — самодокументируемый образец `config.example.toml` коммитим как единый справочник по конфигу: все секции и все поля. **Каждое поле снабжаем комментарием**, из которого ясно: - **зачем** поле — что оно меняет в поведении; - **допустимые значения** — перечисление или границы; - **единицы измерения**, если применимы — секунды, байты, доля `0–1`. ```toml [server] port = # порт HTTP-сервера shutdown_timeout = # ждать мягкой остановки сервера, секунды force_shutdown_timeout = # ждать остановки воркеров, секунды ``` Значения намеренно заменены плейсхолдерами: предмет конвенции — форма комментария, а числа, совпадающие с фактом до цифры, от факта неотличимы и начинают врать молча при смене умолчания. Действующие умолчания и их смысл живут одним домом — таблица «Настройки с числовым значением» в [../database.md](../database.md); `config.example.toml` — источник истины по составу полей. Секретные поля оставляем пустыми — значение приходит из выкладки (см. «Секреты»). *Расхождение:* перечень доверенных адресов в секции `[auth]` образца заполнен примером — подсетью docker, — а не оставлен пустым: пустое значение не говорит, какой формы значение здесь ждут, а сервис с пустым перечнем не поднимается вовсе. Секретов в этой секции больше нет: они ушли 2026-08-22 вместе с собственным входом. Там же, комментарием под секцией, стоит **второе значение перечня — петлевой адрес, под подставной прокси `cmd/devtools proxy`**. Оно стоит закомментированным, и это намеренно: образец описывает боевую выкладку, а локальный вход — способ до неё дойти, и два рабочих значения в одном файле читались бы как выбор без указания, какое из них чьё. ## Поля по дискриминатору `type` Когда набор полей секции зависит от поля-дискриминатора `type` (выбор одного из бекендов или внешних сервисов), обязательность и опциональность полей определяет значение `type`, а не фиксированный список секции. - **Проверка — по `type`.** Для каждого поддерживаемого значения свой набор обязательных полей; поля других значений не требуются. Неизвестное значение — ошибка на старте с перечислением поддерживаемых. - **Образец — по `type`.** В `config.example.toml`: - основное (умолчательное) значение **предзаполнено** рабочими значениями; - альтернативные — **блоками-комментариями ниже**, каждый со своим описанием полей (зачем, границы, единицы — как у обычных полей); - так из примера видны все варианты и поля каждого, не открывая код. Дискриминатора в transcriber пока нет; правило записано на случай второго распознавателя. ## Секреты Секреты доставляет **выкладка**, рендеря их прямо в `config.toml` (transcriber: Ansible из `pet-project-server`). Приложение просто читает TOML — отдельного слоя секретов в коде нет. Источник истины секрета — внешнее хранилище выкладки, не репозиторий и не окружение. - Секретные поля transcriber: `yandex.speech_kit_api_key`, `yandex.object_storage_access_key_id`, `yandex.object_storage_secret_access_key`. Секрет клиента OIDC отсюда ушёл 2026-08-22 вместе с собственным входом. - Отрендеренный `config.toml` (с секретами) **не коммитится**; права `0600`, владелец — пользователь процесса (`1000:1000`). - В `config.example.toml` секретные поля — пустые строки. *Расхождение:* сейчас там стоят подсказки вида `your_..._here`, а не пустые строки, и загрузчик их не отличает от настоящего значения. - Загрузчик на старте проверяет, что обязательные секреты не пусты (ловит криво отрендеренный файл) — см. «Проверка и остановка на старте». - В логи секреты не попадают — см. [logging.md](logging.md), «Безопасность». - **Отказ загрузки настроек не несёт содержимого файла.** Текст такого отказа собирает библиотека разбора, и собирает она его из разбираемого куска: `toml.ParseError` кладёт в сообщение само значение («Invalid float value: %q»). Оборванная кавычка в строке секретного ключа — типовая поломка криво отрендеренного шаблона выкладки — уносит ключ в журнал контейнера целиком, а инвариант «секрет не покидает конфиг» помечен необратимым. Поэтому отказ разбора пересобирается своими словами: путь, строка, столбец и последний ключ, без сообщения библиотеки. Прочие отказы декодера (несовпадение типов, неподдерживаемый тип) собраны из имён ключей и типов, значений в них нет, и их текст остаётся как есть — иначе за разборчивость отказа платили бы там, где платить не за что. ## Проверка и остановка на старте Конфиг проверяем **на старте, до приёма трафика**. Негодный конфиг — лог `ERROR` и выход с ненулевым кодом, не стартуем наполовину. Что проверяем: - обязательные поля заданы; - каталоги хранилища существуют и доступны на запись; - границы числовых полей соблюдены; - ключи внешних сервисов не пусты. *Расхождение:* `LoadConfig` проверяет только существование файла и разбирает TOML. Пустые ключи Yandex ловятся в конструкторе распознавателя, и там процесс выходит с кодом 1. Единого места проверки нет. Два ключа секции `[telegram]`, стоявшие здесь исключением, ушли вместе с самим входом 2026-08-14: секции больше нет, и своей проверки у неё тоже. Секция `[auth]` — первая, у которой проверка своя и стоит на старте: `AuthConfig.Validate()` зовётся из `cmd/transcriber` сразу после загрузки и роняет процесс с именем незаполненного ключа. Причина в цене умолчания: поднявшись с пустым перечнем доверенных адресов, сервис не узнавал бы никого, а узнать об этом было бы неоткуда — все адреса приложения просто отвечали бы отказом. Сообщение называет **имя ключа**; правило «значения в отказ не идут» остаётся в силе для прочих секций, где секреты есть. ## Структура в коде - Весь разбор и проверка — в `internal/config`; наружу отдаётся готовая `Config`. - Одна корневая структура `Config` с под-структурами по секциям. Перечень секций и полей здесь не повторяем: источник истины по составу — `config.example.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.example.toml` у поля стоит значение свежей установки. Первым таким полем был `telegram.enabled`; секция убрана 2026-08-14, и живого примера у правила сейчас нет.